序章
ご存知のとおり、コンピューター プログラミング言語は、主にコンパイル言語とインタプリタ言語の2 つのカテゴリに分類されます。
いわゆるコンパイル言語とは、まずプログラムのソースコードをコンピュータが実行可能なバイトコードにコンパイルし、これらのバイトコードを組み立てて最終的な実行可能ファイルを形成するもので、バイトコードが直接実行されるため、動作効率と速度の点で優れています。通常、コンパイル言語はインタプリタ言語よりもはるかに優れており、コンパイル言語の代表的なものには、C/C++、C#、人気のある Rust や Golang などが含まれます。
通常、インタプリタ言語は最初にインタプリタを実行し、このインタプリタがソース コードを 1 行ずつ実行します。もちろん、コンピュータ自体はバイトコードしか実行できないため、インタプリタはまずソース コードをバイトコードに変換し、次にバイトコードを実行します。インタープリター言語の主な代表は、Python、JavaScript などです。
それらはすべてバイトコードを実行しているのに、なぜインタープリタ型言語はコンパイラのようにすべてのソース コードを一度にコンパイルするのではなく、バイトコードを 1 つずつコンパイルする必要があるのでしょうか? 実際、これはまさにインタープリタ型言語の利点です。コンパイラは、コードを 1 つずつ実行し、1 つずつ実行するため、インタープリター言語はコンテキスト環境をあまり考慮する必要がなく、Python などの言語は実行プロセス内の変数の型を気にする必要がありません。
したがって、日常生活では、多くの人がコンパイル言語を静的型付き言語、インタープリター型言語を動的型付き言語と呼ぶこともあります。
Python プログラムのコンポーネント
典型的なインタープリタ言語として、Python もインタープリタを通じて実行されます。Python
ファイルを実行すると、Python インタープリタは最初にシステム メモリにロードされます。そして、このインタープリタは指定された py ファイル パスに基づき、コード ファイルがロードされます。見つかったものは、上から下、左から右の順序でコンパイルおよび実行されます。
もちろん、このいわゆるコンパイル プロセスは、静的言語のコンパイルとは明らかに少し異なります。まず、
プログラム コードの解析、字句解析、および言語解析のための古典的な環境です。文法解析 このステップにより、インタプリタは実行するコードの抽象構文スパニングツリー、いわゆるASTを取得できるため、実際に何をすべきか、最初に何をすべきか、次に何をすべきかを大まかに理解することができます。これらのコードは次に変換されます。指定された、Python 実行可能ファイル、実行オブジェクト.Code Object.
Python はオブジェクト指向プログラミング言語です。つまり、すべてがオブジェクトです。そして、このオブジェクトはコード オブジェクトです。Python では、Python の観点から、すべての関数、クラス、変数、およびその他の操作可能なものを指します。設計上、これらはすべてコード オブジェクトです。
このコード オブジェクトはインタプリタで直接実行されますか?実際には、そうではありません。
コードを作成するとき、関数が相互に呼び出せることは誰もが知っています。関数、変数、定数などはすべて独自の関数を持ちます。スコープ (実行空間). 通常、独自のスコープ内にないものは操作できません. これを実現するには、インタプリタが異なる必要があります. スコープは分離されています. Python では、分離するために使用される異なるスコープは Frame です
。
Python のフレーム
Python では、関数のすべての入口と出口にはフレーム操作が伴います。Python インタプリタは、従来のスタック構造と同じ呼び出しスタックを維持しており、ほぼ LIFO (Last In First Out) として理解できます。 queue .関数が呼び出されるたびに、フレームが作成され、コール スタックにプッシュされます。関数からリターンされるたびに、フレームがコール スタックからポップされます。Python プログラムをデバッグするとき、プログラムが切断された後
、表示されている関数呼び出しの関係は、実際には Python の呼び出しスタックです。
デバッガの各レベルはフレームを表します。
以下は Python の公式のフレーム定義です
。フレーム自体がコンテナのようなものであることがわかります。現在の実行環境では、ロードされたら、
次のコードを使用してこれらのプロパティを表示します
import inspect
def A():
frame = inspect.currentframe()
print(f"""
f_back: {
frame.f_back}
f_builtins: {
frame.f_builtins}
f_code: {
frame.f_code}
f_globals: {
frame.f_globals}
f_lasti: {
frame.f_lasti}
f_lineno: {
frame.f_lineno}
f_locals: {
frame.f_locals}
f_trace: {
frame.f_trace}
""")
return
def B():
return A()
def C():
return B()
C()
実行結果は次のとおりです。
A 関数が呼び出されると、A 関数のフレームが現在のフレームの前のフレームを記録していることがわかります。つまり、A の関数スコープ内で、前のフレームから現在のフレームまで、つまり、どの関数が A を呼び出したかを示します。同時に、フレームには、コードの実行数や現在のコードがどのファイルにあるかなど、現在実行中の情報も記録されます。さらに興味深いのは
、フレームのトラッキング機能を指定する f_trace 属性です。
以下はトラッキング機能の設定例です。
import sys
def trace_func(frame, event, arg):
print(f"跟随事件 {
event} 目前正处于 {
frame.f_code.co_name} 函数 参数为:{
arg}")
return trace_func
def add(a,b):
print(f"进入add函数,运算结果为:{
a+b}")
return a+b
# 设置跟踪函数
sys.settrace(trace_func)
# 执行函数
add(1,2)
# 取消跟踪函数
sys.settrace(None)
図からわかるように、トラッキング関数のトリガー イベントは 3 つあり、関数の呼び出し時に 1 回 (call)、関数の実行時に 2 回 (line)、関数の終了時に 1 回 (return) トリガーされます。実際に関数が戻る場合は、add 関数の戻り値 3 が最初に取得されます。
このトレース機能を利用すると、プログラムのデバッグ時にプログラムの実行状況を簡単に把握することができますが、実は多くのデバッガでも原理は同じです。
f_builtins からは、len、id、print、max などのよく知られた名前が数多く表示されます。これは、これらの組み込み関数をどこからでも呼び出すことができる理由も説明しています。プログラムが実行されているフレームには、
専用スペース f_builtins は、これらの組み込み関数を格納するために存在します。
よく観察してみると、f_globals にも見覚えのある単語がたくさんあることが分かります 例えば、__name__ の値は __main__ であり、キーは A、B、C の 3 つあり、その値はすべてコードオブジェクト. 実際、この属性は主に現在のフレームのグローバル名前空間に保存されます. つまり、グローバル変数属性関数が保存される場所です. クロージャなどの手法を頻繁に使用する場合は、キーワードがあることを知っておく必要がありますPythonではglobalと呼ばれ、globalを使用して定義された変数はf_globalsに格納され、グローバル変数になります。
グローバル スコープでは、当然ローカル スコープが存在します。これが f_locals の動作です。すべてのローカル変数の属性などは、この f_locals に格納されます。もちろん、前の伏線の後、このローカル変数とグローバル変数も Python 設計の根幹にある紳士協定です。異なるフレームには異なる f_local がありますが、通常の Python 開発者にとって、スコープ分離の役割は実際に達成されています。しかし、実際には、トレース関数などを使用できます。メソッドを使用して、目的のフレームを取得し、そこから必要なものを取得します。
つまり、Python では、必要に応じていつでも、任意の関数内の他の関数やメソッドの変数値、実行条件、実行結果を取得できます。
より興味深い最後のものは f_code で、現在のフレームに実行オブジェクト、つまり、上で述べたようにコンパイル中にインタプリタによって生成されたコード オブジェクトが格納されます。
コードオブジェクト
コード オブジェクトの公式の定義は次のとおりです。
コード オブジェクトは、Python インタープリタの操作単位として理解できます。Python インタープリタがコードの実行、関数の実行、クラスの作成などを行うときはいつでも、対応するコード オブジェクトが見つかります。そしてコードオブジェクトを実行します。
特定の例を使用して、これらの属性が何を表すかを見て
みましょう。実行結果から、Python の実行時に関数 D には合計 3 つのパラメーターがあり、この定数値が 2 つあることも明確にわかります。関数内では、1 つは None で、もう 1 つは 0 です。デフォルトでは None が存在することに注意してください。co_filename、co_firstlineno、co_name の 3 つの属性を通じて、コード内で実行される関数 D が配置されている py を知ることができます。ファイルの具体的な場所、このファイルの最初のコード行の場所はpyファイルの2行目、実行する関数の関数名はDです。このとき、co_flagsの値は67 ですが、公式
ドキュメントでは の定義ではフラグ ビットとして存在するはずですが、なぜここで整数になっているのですか?
コンピュータの CPU の特別なフラグ ビット レジスタと同じように、ここの co_flags もビットごとのフラグのフィールドですここで、「67」の部分は表示の便宜上10進数に変換していますが、その本質は2進数の「1000011」で、左の0ビット目から右の6ビット目までの合計7つの符号で構成されています。フラグ ビットは次のとおりです: たとえば、
最後の
2 ビットは、この関数がジェネレータ関数であるかコルーチンであるかを表します。最後の 3 番目のビットは、この関数がクロージャ関数であるかどうかを表します。最初のビットが 1 の場合、このコードがオブジェクトは
最適化されており、実際にバイトコードを実行するときに高速ストレージ (Load_Fast) を使用します (bytecode)
ここでいうバイトコード(bytecode)とはcodeオブジェクトのco_codeのことを指します、もちろん直接出力することは絶対にできませんが、バイトコードに関してはPython公式にdisモジュールが用意されており簡単に見ることができます。
先ほどまでは D 関数でしたが、dis モジュールを使用してそのバイトコードを確認しますが、
ここでのバイトコードは基本的にソース コード内のコードと 1 対 1 に対応します。
まず、Load_const 0 で定数 0 をスタックにプッシュし、Store_Fast でスタックの先頭 0 を保存し、e として co_varnames に格納します。次に、Load_Fast を使用して変数スタックから 2 つの変数 a と b を取り出し、Binary_Add を使用します
。 2 つの変数を加算し、Store_Fast を使用して加算結果を変数スタックに格納し、c という名前を付けます。次に、変数スタックから c と d を読み取り、Inplace_Add を使用して計算結果を計算し、その結果を c として格納します。 、変数スタックから c の値を取得し、Return_Value を使用して上位層によって呼び出された関数に c を返します。
上記のバイトコード フローから、一見単純なコード行にスタックの内外で多くの演算が含まれていることがわかります。たとえば、Binary_Add は加算演算として使用されますが、バイトコード自体にはオペランドがありません。これは、実際にスタックの先頭に 2 つの要素を追加するためです。したがって、どの変数であるかを指定する必要はありません。事前に Load_Fast を使用して変数をスタックの先頭にプッシュし、直接加算を実行します。手術。
Python バイトコードについて詳しく知りたい場合は、Python の公式ドキュメントで提供されている定義を確認してください。
https://docs.python.org/zh-cn/3/library/dis.html?highlight=dis#python-bytecode-instructions
要約する
上記の紹介を経て、これまでの Python プログラムの実行プロセスを要約できます。
- Python インタープリターを開始し、独自のオペレーティング環境を初期化します。
- 実行する py ファイルを読み取り、構文および字句解析を通じて人間が判読できるソース コードをコード オブジェクトに変換します。
- コードオブジェクトを順番に実行し、その中のバイトコードを実行します。
- 操作の結果を返す