内部からのPython。 はじめに

ボアコンストリクター 1. はじめに
2. オブジェクト。
3. オブジェクト。 しっぽ
4. プロセス構造

標準ライブラリを学習することに加えて、言語が内部からどのように配置されているかを知ることは常に興味深く、時には有用です。 Pythonの開発者の1人であるAndrei Svetlov( svetlov )は、CPythonデバイスに関する一連の記事に興味があるすべての人にアドバイスしています。 最初のエピソードの翻訳をお見せします。

私の友人はかつて私に言った:「あなたは、一部の人々にとって、C言語はアセンブラー命令で展開するマクロのセットにすぎないことを知っている」 それはかなり前のことでした(知っている人なら:はい、 LLVMが登場する前でも)が、私はこれらの言葉をよく覚えていました。 たぶん、 カーニガンとリッチーがCプログラムを見ると、彼らは実際にアセンブラーコードを見るでしょうか? ティム・バーナーズ・リーはどうですか? たぶん、彼は私たちとは違う方法でインターネットをサーフィンしているのでしょうか? そして、結局、 キアヌ・リーブスはその不気味な緑の混乱に何を見ましたか? いいえ、本当に、彼はそこに何を見たのですか?! うーん...プログラムに戻ります。 Guido van RossumはPythonプログラムを読むときに何を見ますか?

この投稿は、Pythonの内部に関する一連の記事の最初の投稿です。 トピックを他の人に説明することが、それを理解する最良の方法だと思います。 そして、Pythonコードの背後にある「気味の悪い緑の混乱」を見て理解することを本当に学びたかったのです。 私は主にCPython 3rdバージョン 、バイトコードについて書きます(私はコンパイル段階のファンではありません)が、 多かれ少なかれ無視しないでしょう。これはあらゆる種類のPythonコードの実行に関連しています、Cythonなど)。 簡潔にするために、特に明記しない限り、 CPythonを意味するPythonを作成します。 また、特に明記しない限り、POSIX互換のOSまたは重要な場合はLinuxを意味します。 Pythonの仕組みに興味がある場合は、この記事を最後まで読むことをお勧めします。 CPythonに貢献したい場合は、これをさらに行う必要があります。 あるいは、私が犯した間違いを見つけるためにこれを行うことができます。これがあなたの感情や感情を表現する唯一の方法である場合、私を笑い、悪意のあるコメントを残すために。

私が書くことのほぼすべては、Pythonのソースコードまたは他のいくつかの優れたソースで見つけることができます(ドキュメント、特にこのページとこのページ、PyConによる個別の講義、 python-devでの検索など)。 すべてを見つけることができますが、すべての資料を1つにまとめ、RSS経由で購読できる私の努力があなたの冒険を促進することを願っています。 読者はC言語に少し精通していると思います。 オペレーティングシステムの理論で。 任意のアーキテクチャのアセンブラで少し。 Pythonでは悪くありませんし、UNIXでも快適です(たとえば、ソースコードの1つを簡単にインストールできます)。 このすべてについて十分な経験がなくても心配する必要はありませんが、簡単な水泳は約束できません。 Pythonを開発するためのカスタマイズされた環境がない場合は、 ここにアクセスして必要な手順に従うことをお勧めします。

おそらく既に知っていることから始めましょう。 メカニズムの隠phorは、何が起こっているのかを理解するのに便利だと思います。 Pythonの場合、Pythonは仮想マシンに依存して動作するため(ほとんどのインタープリター言語と同様)、これは簡単です。 ここでは、「 仮想マシン 」という用語を正しく理解することが重要です。VirtualBoxよりもJVMの方を考える必要があります(技術的には、本質的には同じですが、実際には通常共有されます)。 この用語は文字通り理解しやすいようです-これはプログラムで構成されるメカニズムです。 プロセッサは、入力としてマシンコードとデータを受け取り、状態(レジスタ)を持ち、入力データと現在の状態に基づいて、メモリまたはバスに新しい情報を表示する単なる複雑な電子マシンです。 なるほど? また、CPythonは、状態と処理の命令を持つソフトウェアコンポーネントから組み立てられたメカニズムです(実装が異なると、異なる命令が使用される場合があります)。 このメカニズムは、Pythonインタープリターが配置されているプロセスで機能します。 私は「 メカニズム 」を使ったこの比likeが好きで、すでに詳細に説明しています。

上記を踏まえて、このコマンドを実行すると何が起こるかを鳥瞰的に評価しましょう。

$ python -c 'print("Hello, world!")' 

Pythonバイナリが起動し、標準Cライブラリが初期化され(これはほとんどすべてのプロセスが起動したときに発生します)、main関数が呼び出されます( Py_Mainソース./Modules/python.c参照)。 いくつかの準備手順(引数の解析、環境変数の考慮、標準スレッドでの状況の評価など)の後、./ ./Python/pythonrun.c / Py_InitializePy_Initializeます。 概して、この関数はCPythonマシンの起動に必要な部分を「作成」および収集し、単純に「プロセス」は「Pythonインタープリターを内部に持つプロセス」に変わります。 さらに、 インタープリター 状態ストリーム状態という 2つの非常に重要な構造が作成されます 。 また、 sys組み込みモジュールと、すべての組み込み関数と変数を含むモジュールを初期化します。 次のエピソードでは、これらの手順について詳しく説明します。

これらすべてを備えたPythonは、彼が与えられたものに応じて、いくつかの方法の1つでクロールします:行が実行されます(オプション-c )、モジュールが実行されます(オプション-m )、ファイルが実行されますスクリプトインタプリタとして)またはREPLが起動します(これは、対話型デバイスであるファイルを実行する特殊なケースです)。 この場合、行が実行されます。 -cオプションを渡しました。 この行を実行するにPyRun_SimpleStringFlags . ./Python/pythonrun.c / ./Python/pythonrun.cPyRun_SimpleStringFlagsます。 この関数は、コードの行が実行される__main__ 名前空間を作成します$ python -c 'a=1; print(a)'場合はどこに保存されますか?そうです、このスペースで)。 スペースを作成した後、その中で文字列が実行されます(より正確には、解釈されます)。 これを行うには、最初に文字列をマシンにとって理解可能なものに変換する必要があります。

前にも言ったように、パーサーとコンパイラーPythonには焦点を合わせません。 私はこれらの分野の専門家ではなく、これにはあまり興味がありません。そして、私の知る限り、Pythonコンパイラにはコンパイラに関する大学のコースを超える特別な魔法はありません。 これらのトピックのトップを少し調べて、CPythonの動作のいくつかの機能(たとえば、パーサーに影響を与えるグローバルオペレーター)を検討するために、少し後で戻ってくるかもしれません。 一般に、 PyRun_SimpleStringFlags 解析 /コンパイル段階は次のとおりです。字句解析と解析ツリーの作成、 抽象構文ツリー (AST)への変換 ./ PyAST_FromNode ./Python/ast.cPyAST_FromNodeを使用し AST のコードオブジェクトへのコンパイル。 これで、コードオブジェクトをPython仮想マシンのメカニズムが機能するバイナリ文字列と考えることができます-これで、解釈の準備ができました。

ほとんど空の__main__があり、コードオブジェクトがあり、それを実行したいのです。 次は? ./Python/pythonrun.cからの./Python/pythonrun.crun_mod

 v = PyEval_EvalCode((PyObject*)co, globals, locals); 

この関数は、コードオブジェクトと名前空間のglobalslocals (この場合、新しく作成された名前空間__main__ )を受け入れ、 フレームオブジェクトを作成し実行します。 ストリームの状態を決定するPy_Initializeに戻ります。 各フィードストリームは、(特に)現在実行中のフレームのスタックを示す個別の状態構造によって表されます。 フレームオブジェクトが作成され、ストリームステータススタックの最上部に配置された後、かなり長い関数./Python/ceval.c 、操作(より正確には、それが指すバイトコード)が操作PyEval_EvalFrameExます。

PyEval_EvalFrameExはフレームを受信し、オペレーションコード(および、存在する場合はオペランド。これについては後で説明します)を取得し、オペレーションコードに対応するCコードを実行します。 Pythonコードのスニペットを分解して、これらの「オペレーションコード」がどのように見えるかを見てみましょう。

 >>> from dis import dis # !    ! >>> co = compile("spam = eggs - 1", "<string>", "exec") >>> dis(co) 1 0 LOAD_NAME 0 (eggs) 3 LOAD_CONST 0 (1) 6 BINARY_SUBTRACT 7 STORE_NAME 1 (spam) 10 LOAD_CONST 1 (None) 13 RETURN_VALUE >>> 

...多くの知識がなくても、バイトコードは十分に読み取り可能です。 eggsという名前の何かを「ロード」します(どこからロードしますか?どこからロードしますか?)そして、定数値(1)をロードし、次に「バイナリ減算」を実行します(ここで「バイナリ」という言葉は何を意味しますか?オペランドは何ですか?)、そしてなど。

ご想像のとおり、変数は先ほど見たグローバルおよびローカルのネームスペースからオペランドスタックに「ロード」され(実行可能フレームのスタックと混同しないでください)、正確にバイナリ減算がそれらを引き出し、一方から他方を減算し、結果を出力しますスタックに戻ります。 「2進減算」とは、1つのオペランドを別のオペランドから減算することです(つまり、「バイナリ」、つまり2進数との接続はありません)。

PyEval_EvalFrameExファイルのPyEval_EvalFrameEx関数を./Python/ceval.cで調べることができ./Python/ceval.c 。 それは十分に大きいので、明らかな理由により、ここでは全体を説明しませんが、 BINARY_SUBTRACT操作を処理するときに実行されるコードを示します。

 TARGET(BINARY_SUBTRACT) { PyObject *right = POP(); PyObject *left = TOP(); PyObject *diff = PyNumber_Subtract(left, right); Py_DECREF(right); Py_DECREF(left); SET_TOP(diff); if (diff == NULL) goto error; DISPATCH(); } 

...最初のオペランドをポップし、スタックから2番目のオペランドを取得し、 PyNumber_Subtract C関数の両方のオペランドを渡し、 PyNumber_Subtractを両方のオペランドに理解できないようにし(その後Py_DECREFします)、減算の結果でスタックの一番上の値を書き換え、 diff NULL diffない場合diff DISPATCHを実行しNULL 。 だから。 私たちはまだいくつかのことを理解していませんが、Pythonで最も低いレベルで2つの数値を引くことは一般的に理解できると思います。 しかし、このポイントに達するには、約1万5千語が必要でした!

フレームが完了すると、 PyRun_SimpleStringFlagsは完了コードを返し、メイン関数はクリーニングを実行し( Py_Finalize特に注意しPy_Finalize )、 libcatexitなど)はPy_Finalizeず、プロセスは終了します。

この投稿が非常に有益であり、将来、Pythonのさまざまな部分について議論する際の基盤として使用できることを願っています。 インタプリタ、ストリームの状態、名前空間、モジュール、組み込み関数と変数、コードとフレームオブジェクト、およびBINARY_SUBTRACTハンドラーからのあいまいな語DECREFDISPATCH DECREF用語がDECREFあります。 また、この記事ではさまよっていましたが、名前- オブジェクトという名前ではなかった重要な「ファントム」という用語もあります 。 CPythonオブジェクトシステムは、すべてがどのように機能するかを理解するために重要であり、次の投稿で詳しく説明することを望みます。

連絡を取り合いましょう。

翻訳中に誰かが傷ついた:意味、用語、爬虫類。 一緒に世界をより良い場所にしましょう。コメントに誤りについて書いてください。それはより信頼できます。

ブルカに来てください! 一緒に現代のツールの知恵を学び、クールな製品を作成します。

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


All Articles