Pythonインタープリター:ヘビは何を考えていますか? (パートI-III)

画像

翻訳者から
Pythonインタープリターの構造に関する 3つの 記事シリーズの非常に無料の翻訳。 著者は、このトピックに関する独自の自転車を開発しており、プロセスに現れた知識を共有することにしました。 彼がそれから何を得たのか見てみましょう。


この一連の記事は、一般的にpythonで記述できる人のために設計されていますが、この言語が内部からどのように配置されているかについてはよくわかっていません。 実際、3か月前の私のように。

小さな免責事項:私はpython 2.7インタープリターの例でストーリーをリードします。 後で説明するすべてのものは、Python 3.xで繰り返し、いくつかの関数の構文と命名の違いに合わせて調整できます。

それでは始めましょう。

パートI. Pythonを聞いて、あなたは何を持っていますか?



私たちの最愛のヘビが何であるかについて、少し(実際には、強い)高レベルの見方から始めましょう。 対話型インタープリターでこのような行を入力するとどうなりますか?

>>> a = "hello" 


enterに指を当てると、Pythonは次のプロセスのうち4つを開始します: 字句解析解析コンパイル 、および直接解釈 。 字句解析とは、トークンと呼ばれる特定の文字シーケンスに入力したコード行を解析するプロセスです。 次に、これらのトークンに基づくパーサーは、それに含まれる要素間の関係を表示する構造を生成します(この場合、構造は抽象構文ツリーまたはASDです)。 さらに、ASDを使用して、コンパイラは1つ以上のオブジェクトモジュールを作成し、それらをインタープリターに渡してすぐに実行します。

語彙分析、構文解析、およびコンパイルのトピックを掘り下げることはしませんが、これは主に私自身がそれらについて知らないからです。 代わりに、賢い人々がすべてを正しく行い、これらの手順がエラーなしでPythonインタープリターで機能することを想像してみましょう。 提示? 先に進みます。

オブジェクトモジュール (またはコードオブジェクト、オブジェクトファイル)に進む前に、明確にすることがいくつかあります。 この一連の記事では、関数オブジェクト、オブジェクトモジュール、およびバイトコードについて説明します。これらはすべて、何らかの関連概念がありますが、完全に異なっています。 インタプリタを理解するために関数オブジェクトが何であるかを知る必要はありませんが、それらへの注意を止めたいと思います。 それらが単にクールであるという事実は言うまでもありません。

だから

関数オブジェクトまたはオブジェクトなどの関数



これがPythonプログラミングに関する最初の記事でない場合は、いくつかの「関数オブジェクト」に注意する必要があります。 これはまさに「頭のいい人」が「 ファーストクラスオブジェクトとしての機能」や「Python でのファーストクラス機能の存在」について語る文脈で語られているものです。 次の例を考えてみましょう。

 >>> def foo(a): ... x = 3 ... return x + a ... >>> foo <function foo at 0x107ef7aa0> 


「関数は最初のクラスのオブジェクト」という表現は、リストがオブジェクトであり、 MyObjectクラスのインスタンスがオブジェクトであるという意味で、関数は最初のクラスのオブジェクトであることを意味します。 また、fooはオブジェクトであるため、関数が呼び出される方法に関係なく、それ自体に重要性があります(つまり、 foofoo()は2つの異なるものです)。 fooを別の関数に引数として渡すことができ、それを新しい名前( other_function = foo )に再割り当てできます。 最初のクラスの関数を使用すると、何でもでき、すべてに耐えることができます。

パートII オブジェクトモジュール



この時点で、関数オブジェクトにコードオブジェクトが含まれていることを確認するために、さらに深く調べる必要があります。

 >>> def foo(a): ... x = 3 ... return x + a ... >>> foo <function foo at 0x107ef7aa0> >>> foo.func_code <code object foo at 0x107eeccb0, file "<stdin>", line 1> 


リストからわかるように、オブジェクトモジュールは関数オブジェクトの属性です(他にも多くの属性がありますが、この場合はfooが単純であるため、特に重要ではありません)。

オブジェクトモジュールは、Pythonコンパイラによって生成され、インタープリターに渡されます。 モジュールには、実行に必要なすべての情報が含まれています。 その属性を見てみましょう。

 >>> dir(foo.func_code) ['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames'] 


ご覧のように、それらはたくさんあります。そのため、すべてを検討するわけではありません。例として、最も理解しやすい3つに焦点を当てます。

 >>> foo.func_code.co_varnames ('a', 'x') >>> foo.func_code.co_consts (None, 3) >>> foo.func_code.co_argcount 1 


属性は非常に直感的に見えます:
co_varnames変数名
co_consts関数が知っている値
co_argcount関数が取る引数の数

これはすべて非常に有益ですが、私たちのトピックには少し高すぎるように見えますか? インタープリターがモジュールを直接実行するための指示はどこにありますか? しかし、そのような命令があり、それらはバイトコードで表されます。 後者は、オブジェクトモジュールの属性でもあります。

 >>> foo.func_code.co_code 'd\x01\x00}\x01\x00|\x01\x00|\x00\x00\x17S' 


不明なバイトガベージはどのようなものですか?

パートIII。 バイトコード



おそらく自分で理解しているかもしれませんが、念のため、「バイトコード」と「コードオブジェクト」は2つの異なるものです。1つ目は2つ目の属性で、他の多くのものもあります(パート2を参照)。 この属性はco_codeと呼ばれ、インタープリターが実行するために必要なすべての命令が含まれています。

このバイトコードは何ですか? 名前が示すように、これは単なるバイトのシーケンスです。 コンソールに出力するとき、それはかなりクレイジーに見えるので、 ordを通過させて数値シーケンスに持っていきましょう:

 >>> [ord(b) for b in foo.func_code.co_code] [100, 1, 0, 125, 1, 0, 124, 1, 0, 124, 0, 0, 23, 83] 


したがって、Pythonバイトコードの数値表現が得られました。 インタープリターは、シーケンス内の各バイトを調べて、それに関連付けられた命令を実行します。 バイトコード自体にはPythonオブジェクト、オブジェクト参照などが含まれていないことに注意してください。

CPythonインタープリターファイル(ceval.c)を開くことでバイトコードを理解することができますが、これは行いません。 より正確に、しかし後で。 では、簡単な方法で標準ライブラリのdisモジュールを使用してみましょう。

分解する



分解とは、バイトシーケンスを人間の心によりわかりやすいものに変換することです。 この目的のために、非表示のすべてを詳細に表示するPythonのdisモジュールがあります。 モジュールは実動コードではあまり使用されません。その作業の結果はインタープリターではなく、あなただけが必要とします。

それでは、 disを適用して、ブルカをオブジェクトモジュールから削除しましょう。 これを行うには、 dis.dis関数を使用します。

 >>> def foo(a): ... x = 3 ... return x + a ... >>> import dis >>> dis.dis(foo.func_code) 2 0 LOAD_CONST 1 (3) 3 STORE_FAST 1 (x) 3 6 LOAD_FAST 1 (x) 9 LOAD_FAST 0 (a) 12 BINARY_ADD 13 RETURN_VALUE 


非表示のテキスト
多くの場合、 dis.dis(foo)形式のレコードを見ることができます。 関数オブジェクトは逆アセンブラーに直接渡されます。 これは便利のために行われます; func_codeは、disはfunc_code見つけて分析しfunc_code 。 この例では、プロセスをよりよく理解するために、コードオブジェクトを明示的に渡します。


最初の列の数字は、分析されたソースの行番号です。 2番目の列は、バイトコード内のチームのオフセットを示していますLOAD_CONSTは位置「0」、 STORE_FAST位置「3」などです。 3番目の列は、人間が読める名前のバイト命令を示しています。 これらの名前は、 哀れな小さな人々にのみ必要であり、通訳では使用されません。

最後の2列には、このコマンドの引数に関する詳細が含まれています(ある場合)。 4列目には、オブジェクトモジュールの属性内の引数の位置が反映されます。 この例では、 LOAD_CONST命令のLOAD_CONSTはco_constsリスト属性の最初の位置にあり、STORE_FAST引数はco_varnamesの最初の位置にありco_varnames 。 最後に、5番目の列のdisは、対応する変数の値または名前を反映しています。 実際に確認してください:

 >>> foo.func_code.co_consts[1] 3 >>> foo.func_code.co_varnames[1] 'x' 


これは、 STORE_FASTがバイトコードの3番目の位置にある理由も説明します。バイトコードのどこかに引数がある場合、次の2バイトはその引数を表します。 そのような状況を正しく処理することは、通訳者にもかかっています。

ヒント
BINARY_ADDに引数がないことに突然驚いた場合は、Cookieを注意してください。ただし、事前に心配する必要はありません。 インタプリタ自体について説明するときに、この点に少し戻ります。


バイト(例:100)を意味のある名前(例: LOAD_CONST )に、またその逆にどのように変換しますか? あなた自身がそのようなシステムをどのように編成するか考えてみてください。 「まあ、バイトのシーケンシャル定義があるリスト」や「キーとしての命令名と値としてのバイトを持つ辞書」などの考えがあれば、おめでとうございます-あなたは絶対に正しいです。 それがまさにその仕組みです。 定義自体はopcode.pyファイルで発生し(opcode.hヘッダーファイルも参照できます)、そのような行が約150行表示されます。

 def_op('LOAD_CONST', 100) # Index in const list def_op('BUILD_TUPLE', 102) # Number of tuple items def_op('BUILD_LIST', 103) # Number of list items def_op('BUILD_SET', 104) # Number of set items 


(一部のコメンテーターは、指示の説明を注意深く残しました。)

これで、バイトコード(およびそうでない)のバイトコードと、それを分析するためにdisを使用する方法がわかりました。 次のパートでは、動的PLのままでPythonがどのようにバイトコードにコンパイルできるかを見ていきます。

Source: https://habr.com/ru/post/J206420/


All Articles