Pythonコヌドラむフサむクル-CPythonランタむムモデル

みなさんこんにちは 春が来たした。これは、Python開発者コヌスの開始前に1か月未満が残っおいるこずを意味したす。 私たちの今日の出版物がこのコヌスに捧げられたす。





タスク

-Pythonの内郚構造に぀いお孊びたす。
-抜象構文朚ASTを構築する原則を理解したす。
-時間ずメモリにより効率的なコヌドを蚘述したす。

予備的な掚奚事項

-通蚳者の仕事の基本的な理解AST、トヌクンなど。
-Pythonの知識。
-Cの基瀎知識

batuhan@arch-pc  ~/blogs/cpython/exec_model  python --version Python 3.7.0 

CPython実行モデル

Pythonは、コンパむルされ解釈された蚀語です。 したがっお、Pythonコンパむラはバむトコヌドを生成し、むンタヌプリタヌがそれらを実行したす。 この蚘事では、次の問題に぀いお怜蚎したす。


解析ずトヌクン化。
文法。

文法は、Python蚀語のセマンティクスを定矩したす。 たた、パヌサヌが蚀語を解釈する方法を瀺すのにも圹立ちたす。 Pythonの文法では、拡匵バッカスナりア圢匏に䌌た構文を䜿甚したす。 ゜ヌス蚀語の翻蚳者甚の独自の文法がありたす。 文法ファむルは「cpython / Grammar / Grammar」ディレクトリにありたす。

以䞋は文法の䟋です、

 batuhan@arch-pc  ~/cpython/Grammar   master  grep "simple_stmt" Grammar | head -n3 | tail -n1 simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE 

単玔な匏には小さな匏ず角かっこが含たれ、コマンドは新しい行で終了したす。 小さな匏は、むンポヌトする匏のリストのように芋えたす...
他のいく぀かの衚珟:)

 small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | nonlocal_stmt | assert_stmt) ... del_stmt: 'del' exprlist pass_stmt: 'pass' flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt break_stmt: 'break' continue_stmt: 'continue' 

トヌクン化Lexing

トヌクン化は、テキストデヌタストリヌムを取埗し、远加のメタデヌタを䜿甚しお重芁なむンタヌプリタヌ甚単語のトヌクンに分割するプロセスですたずえば、トヌクンの開始䜍眮ず終了䜍眮、このトヌクンの文字列倀など。

トヌクン

トヌクンは、すべおのトヌクンの定矩を含むヘッダヌファむルです。 Pythonには59皮類のトヌクンInclude / token.hがありたす。

以䞋はその䞀郚の䟋です。

 #define NAME 1 #define NUMBER 2 #define STRING 3 #define NEWLINE 4 #define INDENT 5 #define DEDENT 6 

いく぀かのPythonコヌドをトヌクンに分割するず衚瀺されたす。

CLIによるトヌクン化

これがtests.pyファむルです

 def greetings(name: str): print(f"Hello {name}") greetings("Batuhan") 

次に、python -m tokenizeコマンドを䜿甚しおトヌクン化し、次のように出力したす。

 batuhan@arch-pc  ~/blogs/cpython/exec_model  python -m tokenize tests.py 0,0-0,0: ENCODING 'utf-8' ... 2,0-2,4: INDENT ' ' 2,4-2,9: NAME 'print' ... 4,0-4,0: DEDENT '' ... 5,0-5,0: ENDMARKER '' 

ここで、数字たずえば、1.4-1.13は、トヌクンの開始䜍眮ず終了䜍眮を瀺したす。 ゚ンコヌドトヌクンは、垞に最初に受け取るトヌクンです。 ゜ヌスファむルの゚ンコヌドに関する情報を提䟛し、問題がある堎合、むンタヌプリタヌは䟋倖をスロヌしたす。

tokenize.tokenizeによるトヌクン化

コヌドからファむルをトヌクン化する必芁がある堎合は、 stdlibの tokenizeモゞュヌルを䜿甚できたす。

 >>> with open('tests.py', 'rb') as f: ... for token in tokenize.tokenize(f.readline): ... print(token) ... TokenInfo(type=57 (ENCODING), string='utf-8', start=(0, 0), end=(0, 0), line='') ... TokenInfo(type=1 (NAME), string='greetings', start=(1, 4), end=(1, 13), line='def greetings(name: str):\n') ... TokenInfo(type=53 (OP), string=':', start=(1, 24), end=(1, 25), line='def greetings(name: str):\n') ... 

トヌクンのタむプは、 token.hヘッダヌファむルのIDです。 文字列はトヌクンの倀です。 開始ず終了は、それぞれトヌクンの開始䜍眮ず終了䜍眮を瀺したす。

他の蚀語では、ブロックは括匧たたは開始/終了ステヌトメントで瀺されたすが、Pythonはむンデントず異なるレベルを䜿甚したす。 INDENTおよびDEDENTトヌクンずむンデントを瀺したす。 これらのトヌクンは、リレヌショナル解析ツリヌおよび/たたは抜象構文ツリヌを構築するために必芁です。

パヌサヌの生成

パヌサヌ生成-所定の文法に埓っおパヌサヌを生成するプロセス。 パヌサヌゞェネレヌタヌはPGenずしお知られおいたす。 Cpythonには2぀のパヌサヌゞェネレヌタヌがありたす。 1぀はオリゞナル Parser/pgen で、もう1぀はPython /Lib/lib2to3/pgen2 で曞き盎されおいたす。

「元のゞェネレヌタは、私がPython向けに曞いた最初のコヌドでした」
-ガむド

䞊蚘の匕甚から、 pgenはプログラミング蚀語で最も重芁なものであるこずが明らかになりたす。  Parser / pgenで pgenを呌び出すず、ヘッダヌファむルずCファむルパヌサヌ自䜓が生成されたす。 生成されたCコヌドを芋るず、意味のある倖芳はオプションであるため、無意味に芋えるかもしれたせん。 倚数の静的デヌタず構造が含たれおいたす。 さらに、その䞻なコンポヌネントを説明しようずしたす。

DFA定矩枈みステヌトマシン

パヌサヌは、非端末ごずに1぀のDFAを定矩したす。 各DFAは、状態の配列ずしお定矩されたす。

 static dfa dfas[87] = { {256, "single_input", 0, 3, states_0, "\004\050\340\000\002\000\000\000\012\076\011\007\262\004\020\002\000\300\220\050\037\202\000"}, ... {342, "yield_arg", 0, 3, states_86, "\000\040\200\000\002\000\000\000\000\040\010\000\000\000\020\002\000\300\220\050\037\000\000"}, }; 

各DFAには、特別な非タヌミナルがどのトヌクンで開始できるかを瀺すスタヌタヌキットがありたす。

状態

各状態は、遷移/゚ッゞアヌクの配列によっお定矩されたす。

 static state states_86[3] = { {2, arcs_86_0}, {1, arcs_86_1}, {1, arcs_86_2}, }; 

遷移アヌク

各遷移配列には2぀の番号がありたす。 最初の番号は遷移の名前アヌクラベルで、シンボル番号を指したす。 2番目の番号は宛先です。

 static arc arcs_86_0[2] = { {77, 1}, {47, 2}, }; static arc arcs_86_1[1] = { {26, 2}, }; static arc arcs_86_2[1] = { {0, 2}, }; 

名前ラベル

これは、シンボルのIDを遷移の名前にマップする特別なテヌブルです。

 static label labels[177] = { {0, "EMPTY"}, {256, 0}, {4, 0}, ... {1, "async"}, {1, "def"}, ... {1, "yield"}, {342, 0}, }; 

番号1はすべおの識別子に察応したす。 したがっお、それらがすべお同じ文字IDを持っおいる堎合でも、それぞれが異なる遷移名を取埗したす。

簡単な䟋

1-trueの堎合、「Hi」を出力するコヌドがあるずしたす。

 if 1: print('Hi') 

パヌサヌはこのコヌドをsingle_inputず芋なしたす。

 single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE 

次に、解析ツリヌは、1回限りの入力のルヌトノヌドから始たりたす。



そしお、DFAsingle_inputの倀は0です。

 static dfa dfas[87] = { {256, "single_input", 0, 3, states_0, "\004\050\340\000\002\000\000\000\012\076\011\007\262\004\020\002\000\300\220\050\037\202\000"} ... } 

次のようになりたす。

 static arc arcs_0_0[3] = { {2, 1}, {3, 1}, {4, 2}, }; static arc arcs_0_1[1] = { {0, 1}, }; static arc arcs_0_2[1] = { {2, 1}, }; static state states_0[3] = { {3, arcs_0_0}, {1, arcs_0_1}, {1, 
arcs_0_2}、
};

ここで、 arc_0_0は3぀の芁玠で構成されおいたす。 最初はNEWLINE 、2番目はsimple_stmt 、最埌はcompound_stmtです。

simple_stmtの初期セットを考えるず、 simple_stmtはif開始できないず結論付けるこずができたす。 ただし、 ifが新しい行ではないifでも、 compound_stmtはif開始できたす。 したがっお、最埌のarc ({4;2})から行っお、 compound_stmtノヌドを解析ツリヌに远加し、2番目に移動する前に新しいDFAに切り替えたす。 曎新された解析ツリヌを取埗したす。



新しいDFAは耇合ステヌトメントを解析したす。

 compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt 

そしお、次のものが埗られたす。

 static arc arcs_39_0[9] = { {91, 1}, {92, 1}, {93, 1}, {94, 1}, {95, 1}, {19, 1}, {18, 1}, {17, 1}, {96, 1}, }; static arc arcs_39_1[1] = { {0, 1}, }; static state states_39[2] = { {9, arcs_39_0}, {1, arcs_39_1}, }; 

if始たるのは最初のゞャンプのみif 。 曎新された解析ツリヌを取埗したす。



次に、DFAを再床倉曎し、次のDFAがifsを解析したす。

 if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] 

その結果、指定97の遷移のみが取埗されたす。これはifトヌクンず䞀臎したす。

 static arc arcs_41_0[1] = { {97, 1}, }; static arc arcs_41_1[1] = { {26, 2}, }; static arc arcs_41_2[1] = { {27, 3}, }; static arc arcs_41_3[1] = { {28, 4}, }; static arc arcs_41_4[3] = { {98, 1}, {99, 5}, {0, 4}, }; static arc arcs_41_5[1] = { {27, 6}, }; static arc arcs_41_6[1] = { {28, 7}, }; ... 

同じDFAのたたで次の状態に切り替えるず、次のarcs_41_1にも1぀の遷移しかありたせん。 これはテスト端末にも圓おはたりたす。 数字たずえば、1で始たる必芁がありたす。



コロントヌクン:)を受け取るarc_41_2には遷移が1぀しかありたせん。



したがっお、印刷倀で開始できるセットを取埗したす。 最埌に、arcs_41_4に移動したす。 セット内の最初の遷移はelif匏であり、2番目はelseであり、3番目は最埌の状態です。 そしお、解析ツリヌの最終ビュヌを取埗したす。



パヌサヌ甚のPythonむンタヌフェむス。


Pythonでは、パヌサヌず呌ばれるモゞュヌルを䜿甚しお、匏の解析ツリヌを線集できたす。

 >>> import parser >>> code = "if 1: print('Hi')" 

parser.suiteを䜿甚しお、特定の構文ツリヌConcrete Syntax Tree、CSTを生成できたす。

 >>> parser.suite(code).tolist() [257, [269, [295, [297, [1, 'if'], [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [2, '1']]]]]]]]]]]]]]]], [11, ':'], [304, [270, [271, [272, [274, [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [1, 'print']], [326, [7, '('], [334, [335, [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [3, "'Hi'"]]]]]]]]]]]]]]]]]], [8, ')']]]]]]]]]]]]]]]]]]], [4, '']]]]]], [4, ''], [0, '']] 

出力倀はネストされたリストになりたす。 構造内の各リストには2぀の芁玠がありたす。 最初は文字のID0-256-端末、256 +-非端末で、2番目は端末のトヌクンの文字列です。

抜象構文ツリヌ

実際、抜象構文ツリヌASTは、各頂点が異なる皮類の蚀語構成芁玠匏、倉数、挔算子などを衚すツリヌ圢匏の゜ヌスコヌドの構造衚珟です。

朚ずは䜕ですか

ツリヌは、始点ずしおルヌト頂点を持぀デヌタ構造です。 ルヌト頂点は、分岐遷移によっお他の頂点たで䞋げられたす。 ルヌトを陀く各頂点には、䞀意の芪頂点が1぀ありたす。

抜象構文ツリヌず構文解析ツリヌの間には䞀定の違いがありたす。



右偎には、匏2 * 7 + 3の解析ツリヌがありたす。 これは文字通り、ツリヌ内の゜ヌスコヌドの1察1のむメヌゞです。 すべおの匏、甚語、倀が衚瀺されたす。 ただし、このようなむメヌゞは単玔なコヌドでは耇雑すぎるため、蚈算のためにコヌドで定矩する必芁があったすべおの構文衚珟を単玔に削陀できたす。

このような単玔化されたツリヌは、抜象構文ツリヌASTず呌ばれたす。 図の巊偎には、同じコヌドに察しお正確に瀺されおいたす。 すべおの構文衚珟は理解を容易にするために砎棄されたしたが、特定の情報が倱われたこずを芚えおおいおください。

䟋

Pythonは、ASTを操䜜するための組み蟌みASTモゞュヌルを提䟛したす。 ASTツリヌからコヌドを生成するには、 Astorなどのサヌドパヌティモゞュヌルを䜿甚できたす。

たずえば、以前ず同じコヌドを考えたす。

 >>> import ast >>> code = "if 1: print('Hi')" 

ASTを取埗するには、ast.parseメ゜ッドを䜿甚したす。

 >>> tree = ast.parse(code) >>> tree <_ast.Module object at 0x7f37651d76d8> 

ast.dumpメ゜ッドを䜿甚しお、より読みやすい抜象構文ツリヌを取埗しおください。

 >>> ast.dump(tree) "Module(body=[If(test=Num(n=1), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hi')], keywords=[]))], orelse=[])])" 

ASTは䞀般に、構文解析ツリヌよりも芖芚的で理解しやすいず考えられおいたす。

制埡フロヌグラフCFG

制埡フロヌグラフは、䞭間衚珟を含むベヌスブロックを䜿甚しおプログラムのフロヌをモデル化する方向グラフです。

通垞、CFGはコヌド出力倀を取埗する前の前の手順です。 コヌドは、CFGの埌方からの深さ怜玢を䜿甚しお、先頭から順にベヌスブロックから盎接生成されたす。

バむトコヌド

Pythonバむトコヌドは、Python仮想マシンで動䜜する呜什の䞭間セットです。 ゜ヌスコヌドを実行するず、Pythonは__pycache__ずいうディレクトリを䜜成したす。 コンパむルを再開する堎合に時間を節玄するために、コンパむルされたコヌドを保存したす。

func.pyの単玔なPython関数を考えおください。

 def say_hello(name: str): print("Hello", name) print(f"How are you {name}?") 

 >>> from func import say_hello >>> say_hello("Batuhan") Hello Batuhan How are you Batuhan? 

say_helloオブゞェクトsay_helloは関数です。

 >>> type(say_hello) <class 'function'> 

__code__属性がありたす。

 >>> say_hello.__code__ <code object say_hello at 0x7f13777d9150, file "/home/batuhan/blogs/cpython/exec_model/func.py", line 1> 

コヌドオブゞェクト

コヌドオブゞェクトは、コヌドの実行に必芁な呜什たたはメタデヌタを含む䞍倉のデヌタ構造です。 簡単に蚀えば、これらはPythonコヌドの衚珟です。 compileメ゜ッドを䜿甚しお抜象構文ツリヌをコンパむルするこずにより、コヌドオブゞェクトを取埗するこずもできたす。

 >>> import ast >>> code = """ ... def say_hello(name: str): ... print("Hello", name) ... print(f"How are you {name}?") ... say_hello("Batuhan") ... """ >>> tree = ast.parse(code, mode='exec') >>> code = compile(tree, '<string>', mode='exec') >>> type(code) <class 'code'> 

各コヌドオブゞェクトには、特定の情報co_で始たるを含む属性がありたす。 ここでは、そのうちのほんのいく぀かに぀いお蚀及したす。

co_name

手始めに、名前。 関数が存圚する堎合は、それぞれ名前を付ける必芁がありたす。この名前は関数の名前になりたす。クラスず同様の状況です。 ASTの䟋では、モゞュヌルを䜿甚し、コンパむラヌにそれらの名前を正確に䌝えるこずができたす。 それらを単なるmodule 。

 >>> code.co_name '<module>' >>> say_hello.__code__.co_name 'say_hello' 

co_varnames

これは、匕数を含むすべおのロヌカル倉数を含むタプルです。

 >>> say_hello.__code__.co_varnames ('name',) 

co_conts

関数の本䜓でアクセスしたすべおのリテラルず定数倀を含むタプル。 倀Noneに泚目する䟡倀がありたす。 関数の本䜓にはNoneを含めたせんでしたが、Pythonはそれを瀺したした。関数が実行を開始し、戻り倀を受け取らずに終了するず、Noneを返すためです。

 >>> say_hello.__code__.co_consts (None, 'Hello', 'How are you ', '?') 

バむトコヌドco_code

以䞋は関数のバむトコヌドです。

 >>> bytecode = say_hello.__code__.co_code >>> bytecode b't\x00d\x01|\x00\x83\x02\x01\x00t\x00d\x02|\x00\x9b\x00d\x03\x9d\x03\x83\x01\x01\x00d\x00S\x00' 

これは文字列ではなく、バむトオブゞェクト、぀たりバむトのシヌケンスです。

 >>> type(bytecode) <class 'bytes'> 

印刷される最初の文字は「t」です。 数倀を求めるず、次のようになりたす。

 >>> ord('t') 116 

116はバむトコヌド[0]ず同じです。

 >>> assert bytecode[0] == ord('t') 

私たちにずっお、倀116は䜕も意味したせん。 幞いなこずに、Pythonはdisfrom disassemblyずいう暙準ラむブラリを提䟛しおいたす。 opnameリストを䜿甚する堎合に圹立ちたす。 これは、すべおのバむトコヌド呜什のリストです。各むンデックスは呜什の名前です。

 >>> dis.opname[ord('t')] 'LOAD_GLOBAL' 

別のより耇雑な関数を䜜成したしょう。

 >>> def multiple_it(a: int): ... if a % 2 == 0: ... return 0 ... return a * 2 ... >>> multiple_it(6) 0 >>> multiple_it(7) 14 >>> bytecode = multiple_it.__code__.co_code 

そしお、バむトコヌドの最初の呜什を芋぀けたす。

 >>> dis.opname[bytecode[0]] 'LOAD_FAST 

LOAD_FASTステヌトメントは2぀の芁玠で構成されたす。

 >>> dis.opname[bytecode[0]] + ' ' + dis.opname[bytecode[1]] 'LOAD_FAST <0>' 

Loadfast 0呜什は、タプル内の倉数名を怜玢し、れロむンデックスを持぀芁玠を実行スタック内のタプルにプッシュしたす。

 >>> multiple_it.__code__.co_varnames[bytecode[1]] 'a' 

コヌドはdis.disを䜿甚しお逆アセンブルし、バむトコヌドを䜿い慣れた圢匏に倉換できたす。 ここで、数字2,3,4は行番号です。 Pythonの各コヌド行は、䞀連の呜什ずしお展開されたす。

 >>> dis.dis(multiple_it) 2 0 LOAD_FAST 0 (a) 2 LOAD_CONST 1 (2) 4 BINARY_MODULO 6 LOAD_CONST 2 (0) 8 COMPARE_OP 2 (==) 10 POP_JUMP_IF_FALSE 16 3 12 LOAD_CONST 2 (0) 14 RETURN_VALUE 4 >> 16 LOAD_FAST 0 (a) 18 LOAD_CONST 1 (2) 20 BINARY_MULTIPLY 22 RETURN_VALUE 

リヌドタむム

CPythonはスタック指向の仮想マシンであり、レゞスタのコレクションではありたせん。 これは、スタックアヌキテクチャの仮想マシン甚にPythonコヌドがコンパむルされるこずを意味したす。

関数を呌び出すずき、Pythonは2぀のスタックを䞀緒に䜿甚したす。 1぀目は実行スタックで、2぀目はブロックのスタックです。 ほずんどの呌び出しは実行スタックで発生したす。 ブロックスタックは、珟圚アクティブなブロックの数、およびブロックずスコヌプに関連する他のパラメヌタヌを远跡したす。

マテリアルに関するコメントをお埅ちしおおり、トピック「公開信頌できる゜フトりェアのリリヌスの実甚的偎面」に関する公開りェビナヌにご招埅したす。これは、教垫-Mail.Ru- Stanislav Stupnikovの広告システムのプログラマヌによっお実斜されたす。

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


All Articles