Pythonがどのように機能するかを理解しようとしています(私はいつもそれを使用しているためです!)。私の理解では、python script.pyのようなものを実行すると、スクリプトはバイトコードに変換され、インタプリタ/ VM/CPython(実際にはCプログラムのみ)がpythonを読み取ります_バイトコードに応じてプログラムを実行します。
このバイトコードはどのように読み込まれますか? Cでテキストファイルを読み取る方法と似ていますか? Pythonコードがどのようにマシンコードに変換されるのかわかりません。 Pythonインタープリター(CLIのpythonコマンド)は、実際にはマシンコードに変換されたpythonバイトコードファイルは、そのプログラムを介して配置されますか?言い換えると、私のPythonプログラムが実際にマシンコードに変換されることはありませんか? pythonインタープリターはすでにマシンコードに含まれているので、スクリプトは必ずしもそうである必要はありませんか?
はい、あなたの理解は正しいです。基本的に(非常に基本的に)CPythonインタープリター内には、「現在のオペコードがそうである場合、これを行う」という巨大なswitchステートメントがあります。
http://hg.python.org/cpython/file/3.3/Python/ceval.c#l79
Pypyなどの他の実装では、JITコンパイルが行われます。つまり、Pythonをオンザフライでマシンコードに変換します。
一部のコード(ソースコード、ライブ関数オブジェクト、コードオブジェクトなど)のバイトコードを確認したい場合は、 dis
モジュールが必要なものを正確に伝えます。例えば:
>>> dis.dis('i/3')
1 0 LOAD_NAME 0 (i)
3 LOAD_CONST 0 (3)
6 BINARY_TRUE_DIVIDE
7 RETURN_VALUE
dis
docsは、各バイトコードの意味を説明しています。たとえば、 LOAD_NAME
:
co_names[namei]
に関連付けられた値をスタックにプッシュします。
これを理解するには、バイトコードインタープリターが仮想 スタックマシン であり、co_names
が何であるかを知っている必要があります。 inspect
モジュールのドキュメントには、最も重要な内部オブジェクトの最も重要な属性を示す素敵な表があるので、co_names
はcode
ローカル変数の名前のタプルを保持するオブジェクト。言い換えると、LOAD_NAME 0
は0番目のローカル変数に関連付けられた値をプッシュします(そしてdis
はこれを役に立ち、0番目のローカル変数の名前が'i'
であることがわかります)。
そして、バイトコードの文字列だけでは不十分であることを確認するにはそれで十分です。インタープリターは、コードオブジェクトの他の属性、および場合によっては関数オブジェクトの属性も必要とします(ローカルおよびグローバル環境のソースでもあります)。
inspect
モジュールには、ライブコードの調査に役立ついくつかのツールもあります。
これは多くの興味深いものを理解するのに十分です。たとえば、Pythonは、関数の変数がローカル、クロージャ、またはグローバルのいずれであるかを、コンパイル時に関数本体のどこに割り当てたかに基づいて(そしてnonlocal
またはglobal
ステートメントの場合); 3つの異なる関数を記述し、それらの逆アセンブリ(および関連する他の属性)を比較すると、それが何をしているのかを正確に簡単に理解できます。
(ここで少し注意が必要なのは、クロージャーセルを理解することです。これを実際に行うには、3つのレベルの関数が必要です。真ん中のものが最も内側のものがどのように転送されるかを確認するには)。
バイトコードがどのように解釈されるか、およびスタックマシンがどのように機能するか(CPythonの場合)を理解するには、 ceval.c
ソースコードを確認する必要があります。 thy435とeyquemの回答はすでにこれをカバーしています。
pyc
ファイルの読み取り方法を理解するには、もう少し情報が必要です。 Ned Batchelderは 。pycファイルの構造 と呼ばれる素晴らしい(少し古くなっている場合でも)ブログ投稿を公開しています。 (3.3では、インポートに関連する悪質なコードの一部がCからPythonに移動されたため、追跡がはるかに簡単になりました。)ただし、基本的には、ヘッダー情報とモジュールのcode
オブジェクトのみです。 marshal
によってシリアル化されます。
ソースがバイトコードにコンパイルされる方法を理解するには、それが楽しい部分です。
CPythonのコンパイラの設計 は、すべてがどのように機能するかを説明します。 ( Python開発者ガイド の他のセクションの一部も役立ちます。)
初期のもの(トークン化と解析)の場合は、 ast
モジュールを使用して、実際のコンパイルを実行するタイミングにジャンプできます。次に、そのASTがバイトコードに変換される方法について compile.c
を参照してください。
マクロを処理するのは少し難しいかもしれませんが、コンパイラがスタックを使用してブロックに降りる方法、およびそれらのcompiler_addop
とそのフレンドを使用して現在のレベルでバイトコードを出力する方法の概念を理解すると、すべてが理にかなっています。
ほとんどの人が最初に驚かせるのは、関数の動作方法です。関数定義の本体はコードオブジェクトにコンパイルされます。次に、関数定義自体がコードにコンパイルされ(囲んでいる関数本体、モジュールなどの内部)、実行時にそのコードオブジェクトから関数オブジェクトが構築されます。 (クロージャーがどのように機能する必要があるかを考えると、それがなぜそのように機能するかは明らかです。クロージャーの各インスタンスは、同じコードオブジェクトを持つ個別の関数オブジェクトです。)
これで、CPythonにパッチを適用して独自のステートメントを追加する準備ができましたよね?まあ、 Changing CPython's Grammar が示すように、正しいことはたくさんあります(新しいオペコードを作成する必要がある場合は、さらに多くの機能があります)。 PyPy やCPythonを学び、最初にPyPyのハッキングを開始し、自分がやっていることが理にかなっていて実行可能であることがわかった場合にのみ、CPythonに戻ってくる方が簡単かもしれません。
Thg4535の回答を読んだら、ceval.cに関する以下の説明がおもしろいと思うはずです。 Hello、ceval.c!
この記事は、私が一種のファンであるYaniv Akninによって書かれたシリーズの一部です。 Python's Innards