Siklus Hidup Kode Python - Model Runtime CPython

Halo semuanya! Musim semi telah tiba, yang berarti bahwa kurang dari satu bulan tersisa sebelum dimulainya kursus Pengembang Python . Untuk kursus ini publikasi kami hari ini akan dikhususkan.





Tugas:

- Pelajari tentang struktur internal Python;
- Memahami prinsip-prinsip membangun pohon sintaksis abstrak (AST);
- Tulis kode yang lebih efisien dalam waktu dan memori.

Rekomendasi awal:

- Pemahaman dasar juru bahasa (AST, token, dll.).
- Pengetahuan tentang Python.
- Pengetahuan dasar C.

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

Model Eksekusi CPython

Python adalah bahasa yang dikompilasi dan ditafsirkan. Dengan demikian, kompiler Python menghasilkan bytecodes, dan interpreter mengeksekusinya. Selama artikel kami akan mempertimbangkan masalah-masalah berikut:

  • Parsing dan tokenization:
  • Transformasi dalam AST;
  • Grafik Alir Kontrol (CFG);
  • Bytecode
  • Berjalan di mesin virtual CPython.

Parsing dan tokenization.
Tata bahasa.

Grammar mendefinisikan semantik bahasa Python. Ini juga berguna dalam menunjukkan cara pengurai harus menafsirkan bahasa. Tata bahasa Python menggunakan sintaksis yang mirip dengan bentuk Backus-Naur yang diperluas. Ini memiliki tata bahasa sendiri untuk penerjemah dari bahasa sumber. Anda dapat menemukan file tata bahasa di direktori "cpython / Grammar / Grammar".

Berikut ini adalah contoh tata bahasa,

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

Ekspresi sederhana berisi ekspresi kecil dan tanda kurung, dan perintah berakhir dengan baris baru. Ekspresi kecil terlihat seperti daftar ekspresi yang mengimpor ...
Beberapa ungkapan lain :)

 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' 

Tokenisasi (Penggabungan)

Tokenisasi adalah proses memperoleh aliran data teks dan membelah menjadi token kata-kata penting (untuk penerjemah) dengan metadata tambahan (misalnya, di mana token dimulai dan berakhir, dan berapa nilai string dari token ini).

Token

Token adalah file header yang berisi definisi semua token. Ada 59 jenis token di Python (Sertakan / token.h).

Di bawah ini adalah contoh dari beberapa di antaranya,

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

Anda melihat mereka jika Anda memecah beberapa kode Python menjadi token.

Tokenisasi dengan CLI

Ini file tests.py

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

Kemudian kita tokenize menggunakan perintah python -m tokenize dan berikut ini adalah output:

 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 '' 

Di sini angka (misalnya, 1.4-1.13) menunjukkan di mana token dimulai dan di mana berakhir. Token penyandian selalu merupakan token pertama yang kami terima. Ini memberikan informasi tentang penyandian file sumber, dan jika ada masalah dengan itu, penerjemah melempar pengecualian.

Tokenisasi dengan tokenize.tokenize

Jika Anda perlu tokenize file dari kode Anda, Anda dapat menggunakan modul tokenize dari stdlib .

 >>> 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') ... 

Jenis token adalah id di file header token.h. String adalah nilai token. Mulai dan Akhir menunjukkan di mana token dimulai dan berakhir, masing-masing.

Dalam bahasa lain, blok ditunjukkan oleh tanda kurung atau pernyataan awal / akhir, tetapi Python menggunakan indentasi dan level yang berbeda. INDEN dan DENEN dan menunjukkan indentasi. Token ini diperlukan untuk membangun pohon parsing relasional dan / atau pohon sintaksis abstrak.

Generasi Parser

Generasi Parser - proses yang menghasilkan parser sesuai dengan tata bahasa yang diberikan. Generator Parser dikenal sebagai PGen. Cpython memiliki dua generator parser. Satu adalah asli ( Parser/pgen ) dan satu ditulis ulang dengan python ( /Lib/lib2to3/pgen2 ).

"Generator asli adalah kode pertama yang saya tulis untuk Python"
-Guido

Dari kutipan di atas, menjadi jelas bahwa pgen adalah hal yang paling penting dalam bahasa pemrograman. Ketika Anda memanggil pgen (dalam Parser / pgen ) itu menghasilkan file header dan file-C (pengurai itu sendiri). Jika Anda melihat kode C yang dihasilkan, itu mungkin tampak tidak berguna, karena penampilannya yang bermakna adalah opsional. Ini berisi banyak data dan struktur statis. Selanjutnya, kami akan mencoba menjelaskan komponen utamanya.

DFA (Mesin Defined State)

Parser mendefinisikan satu DFA untuk setiap nonterminal. Setiap DFA didefinisikan sebagai susunan status.

 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"}, }; 

Untuk setiap DFA, ada starter kit yang menunjukkan token mana yang bukan terminal bisa dimulai.

Ketentuan

Setiap negara ditentukan oleh array transisi / tepi (busur).

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

Transisi (busur)

Setiap larik transisi memiliki dua angka. Nomor pertama adalah nama transisi (label busur), ini mengacu pada nomor simbol. Angka kedua adalah tujuannya.

 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}, }; 

Nama (label)

Ini adalah tabel khusus yang memetakan id karakter ke nama transisi.

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

Angka 1 sesuai dengan semua pengidentifikasi. Dengan demikian, masing-masing mendapat nama transisi yang berbeda, bahkan jika mereka semua memiliki karakter id yang sama.

Contoh sederhana:

Misalkan kita memiliki kode yang menampilkan "Hai" jika 1 - true:

 if 1: print('Hi') 

Parser menganggap kode ini sebagai single_input.

 single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE 

Kemudian pohon parsing kita mulai pada simpul akar dari input satu kali.



Dan nilai DFA kami (single_input) adalah 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"} ... } 

Ini akan terlihat seperti ini:

 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},
};

Di sini arc_0_0 terdiri dari tiga elemen. Yang pertama adalah NEWLINE , yang kedua adalah simple_stmt , dan yang terakhir adalah compound_stmt .

Dengan set awal simple_stmt kita dapat menyimpulkan bahwa simple_stmt tidak dapat dimulai dengan if . Namun, meskipun jika bukan baris baru, maka compound_stmt masih dapat dimulai dengan if . Jadi, kita beralih ke arc ({4;2}) terakhir arc ({4;2}) dan menambahkan node compound_stmt ke parsing tree dan beralih ke DFA baru sebelum melanjutkan ke nomor 2. Kami mendapatkan pohon parsing yang diperbarui:



DFA baru mem-parsing pernyataan majemuk.

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

Dan kami mendapatkan yang berikut:

 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}, }; 

Hanya lompatan pertama yang bisa dimulai dengan if . Kami mendapatkan pohon parsing yang diperbarui.



Kemudian kita kembali mengubah DFA dan ifs DFA berikutnya.

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

Akibatnya, kami mendapatkan satu-satunya transisi dengan penunjukan 97. Ini bertepatan dengan token 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}, }; ... 

Saat kami beralih ke status berikutnya, tetap dalam DFA yang sama, arcs_41_1 berikutnya juga hanya memiliki satu transisi. Ini berlaku untuk terminal uji. Itu harus dimulai dengan angka (misalnya, 1).



Hanya ada satu transisi di arc_41_2 yang mendapat token titik dua (:).



Jadi kita mendapatkan satu set yang bisa dimulai dengan nilai cetak. Akhirnya, buka arcs_41_4. Transisi pertama dalam set adalah ekspresi elif, yang kedua adalah yang lain, dan yang ketiga adalah untuk keadaan terakhir. Dan kita mendapatkan tampilan akhir dari pohon parsing.



Antarmuka Python untuk parser.


Python memungkinkan Anda untuk mengedit pohon parsing untuk ekspresi menggunakan modul yang disebut parser.

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

Anda dapat menghasilkan pohon sintaksis tertentu (Sintaksis Beton Pohon, CST) menggunakan parser.suite.

 >>> 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, '']] 

Nilai output akan menjadi daftar bersarang. Setiap daftar dalam struktur memiliki 2 elemen. Yang pertama adalah id karakter (0-256 - terminal, 256+ - non-terminal) dan yang kedua adalah string token untuk terminal.

Pohon sintaksis abstrak

Bahkan, pohon sintaksis abstrak (AST) adalah representasi struktural dari kode sumber dalam bentuk pohon, di mana setiap simpul menunjukkan berbagai jenis konstruksi bahasa (ekspresi, variabel, operator, dll.)

Apa itu pohon?

Pohon adalah struktur data yang memiliki simpul akar sebagai titik awal. Simpul akar diturunkan oleh cabang (transisi) ke simpul lain. Setiap simpul, kecuali root, memiliki satu simpul induk unik.

Ada perbedaan tertentu antara pohon sintaksis abstrak dan pohon parsing.



Di sebelah kanan adalah pohon penguraian untuk ekspresi 2 * 7 + 3. Ini secara harfiah adalah gambar satu-ke-satu dari kode sumber dalam bentuk pohon. Semua ekspresi, istilah, dan nilai terlihat di sana. Namun, gambar seperti itu terlalu rumit untuk sepotong kode sederhana, jadi kita bisa menghapus semua ekspresi sintaksis yang harus kita tentukan dalam kode untuk perhitungan.

Pohon yang disederhanakan seperti itu disebut pohon sintaksis abstrak (AST). Di sebelah kiri pada gambar ditampilkan persis untuk kode yang sama. Semua ekspresi sintaksis telah dibuang untuk memudahkan pemahaman, tetapi ingat bahwa informasi tertentu telah hilang.

Contoh

Python menawarkan modul AST bawaan untuk bekerja dengan AST. Untuk menghasilkan kode dari pohon AST, Anda dapat menggunakan modul pihak ketiga, misalnya Astor .

Misalnya, pertimbangkan kode yang sama seperti sebelumnya.

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

Untuk mendapatkan AST, kami menggunakan metode ast.parse.

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

Coba gunakan metode ast.dump untuk mendapatkan pohon sintaksis abstrak yang lebih mudah dibaca.

 >>> 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 umumnya dianggap lebih visual dan mudah dipahami daripada pohon parsing.

Control Flow Graf (CFG)

Grafik aliran kendali adalah grafik arah yang memodelkan aliran suatu program menggunakan blok-blok dasar yang berisi representasi perantara.

Biasanya, CFG adalah langkah sebelumnya sebelum mendapatkan nilai kode output. Kode dihasilkan langsung dari blok dasar menggunakan pencarian kedalaman mundur di CFG, mulai dari atas.

Bytecode

Bytecode Python adalah seperangkat instruksi antara yang bekerja di mesin virtual Python. Ketika Anda menjalankan kode sumber, Python membuat direktori bernama __pycache__. Ini menyimpan kode yang dikompilasi untuk menghemat waktu jika memulai kembali kompilasi.

Pertimbangkan fungsi Python sederhana di func.py.

 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 objek say_hello adalah fungsi.

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

Ini memiliki atribut __code__ .

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

Objek Kode

Objek kode adalah struktur data yang tidak dapat diubah yang berisi instruksi atau metadata yang diperlukan untuk menjalankan kode. Sederhananya, ini adalah representasi dari kode Python. Anda juga bisa mendapatkan objek kode dengan mengkompilasi pohon sintaksis abstrak menggunakan metode kompilasi .

 >>> 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'> 

Setiap objek kode memiliki atribut yang berisi informasi tertentu (diawali dengan co_). Di sini kami hanya menyebutkan beberapa saja.

co_name

Untuk memulai - sebuah nama. Jika suatu fungsi ada, maka itu harus memiliki nama, masing-masing, nama ini akan menjadi nama fungsi, situasi yang mirip dengan kelas. Dalam contoh AST, kami menggunakan modul, dan kami dapat memberi tahu kompiler apa yang ingin kami beri nama. Biarkan mereka menjadi module .

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

co_varnames

Ini adalah tuple yang berisi semua variabel lokal, termasuk argumen.

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

co_conts

Sebuah tuple yang berisi semua nilai literal dan konstan yang kami akses di tubuh fungsi. Perlu dicatat nilai Tidak Ada. Kami tidak memasukkan Tidak Ada di tubuh fungsi, bagaimanapun, Python menunjukkannya, karena jika fungsi mulai mengeksekusi dan kemudian berakhir tanpa menerima nilai kembali, itu akan mengembalikan Tidak ada.

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

Bytecode (co_code)

Berikut ini adalah bytecode dari fungsi kami.

 >>> 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' 

Ini bukan string, ini adalah objek byte, yaitu urutan byte.

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

Karakter pertama yang dicetak adalah "t". Jika kami meminta nilai numeriknya, kami mendapatkan yang berikut ini.

 >>> ord('t') 116 

116 sama dengan bytecode [0].

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

Bagi kami, nilai 116 tidak berarti apa-apa. Untungnya, Python menyediakan perpustakaan standar yang disebut dis (from disassembly). Ini berguna saat bekerja dengan daftar opname. Ini adalah daftar semua instruksi bytecode, di mana setiap indeks adalah nama instruksi.

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

Mari kita buat fungsi lain yang lebih kompleks.

 >>> 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 

Dan kami menemukan instruksi pertama dalam bytecode.

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

Pernyataan LOAD_FAST terdiri dari dua elemen.

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

Instruksi Loadfast 0 terjadi untuk mencari nama variabel dalam sebuah tuple dan mendorong elemen dengan indeks nol ke dalam tuple dalam tumpukan eksekusi.

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

Kode kami dapat dibongkar menggunakan dis.dis dan mengonversi bytecode ke tampilan yang sudah dikenal. Di sini angka (2,3,4) adalah nomor baris. Setiap baris kode dalam Python diperluas sebagai seperangkat instruksi.

 >>> 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 

Lead time

CPython adalah mesin virtual yang berorientasi pada tumpukan, bukan kumpulan register. Ini berarti bahwa kode Python dikompilasi untuk mesin virtual dengan arsitektur stack.

Saat memanggil fungsi, Python menggunakan dua tumpukan bersamaan. Yang pertama adalah tumpukan eksekusi, dan yang kedua adalah tumpukan blok. Sebagian besar panggilan terjadi pada tumpukan eksekusi; tumpukan blok melacak berapa banyak blok yang aktif saat ini, serta parameter lain yang terkait dengan blok dan cakupan.

Kami menantikan komentar Anda tentang materi dan mengundang Anda ke webinar terbuka tentang topik: "Lepaskan: aspek praktis dari merilis perangkat lunak yang dapat diandalkan", yang akan dilakukan oleh guru kami, pemrogram sistem iklan di Mail.Ru, Stanislav Stupnikov .

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


All Articles