Di dalam mesin virtual Python. Bagian 1



Halo semuanya. Saya akhirnya memutuskan untuk mencari tahu cara kerja juru bahasa Python. Untuk melakukan ini, ia mulai mempelajari satu buku artikel dan pada saat yang sama menerjemahkannya ke dalam bahasa Rusia. Faktanya adalah bahwa terjemahan tidak memungkinkan Anda untuk melewatkan kalimat yang tidak dapat dimengerti dan kualitas asimilasi materi meningkat.Saya minta maaf sebelumnya untuk kemungkinan ketidakakuratan. Saya selalu berusaha menerjemahkan seakurat mungkin, tetapi salah satu masalah utama: sama sekali tidak disebutkan beberapa istilah dalam padanan bahasa Rusia.

Catatan Terjemahan
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

pengantar


Bahasa pemrograman Python telah ada selama beberapa waktu. Pengembangan versi pertama dimulai oleh Guido Van Rossum pada tahun 1989, dan sejak itu bahasa tersebut telah berkembang dan menjadi salah satu yang paling populer. Python digunakan dalam berbagai aplikasi: dari antarmuka grafis ke aplikasi analisis data.

Tujuan artikel ini adalah untuk pergi di belakang layar dari juru bahasa dan memberikan tinjauan konsep tentang bagaimana program yang ditulis dengan Python dieksekusi. CPython akan dipertimbangkan dalam artikel, karena pada saat penulisan, ini adalah implementasi Python yang paling populer dan mendasar.

Python dan CPython digunakan sebagai sinonim dalam teks ini, tetapi penyebutan Python berarti CPython (versi python diimplementasikan dalam C). Implementasi lain termasuk PyPy (python diimplementasikan dalam subset Python terbatas), Jython (implementasi pada Java Virtual Machine), dll.

Saya suka membagi pelaksanaan program Python menjadi dua atau tiga langkah utama (tercantum di bawah), tergantung pada bagaimana penerjemah dipanggil. Langkah-langkah ini akan dibahas dalam beberapa derajat dalam artikel ini:

  1. Inisialisasi - langkah ini melibatkan pengaturan berbagai struktur data yang diperlukan oleh proses python. Kemungkinan besar ini akan terjadi ketika program dijalankan dalam mode non-interaktif melalui cangkang penerjemah.
  2. — , : , , .
  3. — .

Mekanisme untuk menghasilkan pohon "parsing", serta pohon sintaksis abstrak (ASD), adalah bahasa independen. Oleh karena itu, kami tidak akan membahas topik ini terlalu banyak, karena metode yang digunakan dalam Python mirip dengan metode bahasa pemrograman lainnya. Di sisi lain, proses membangun tabel simbol dan objek kode dari ADS dengan Python lebih spesifik, oleh karena itu, perlu mendapat perhatian khusus. Ini juga membahas interpretasi objek kode yang dikompilasi dan semua struktur data lainnya. Topik yang dibahas oleh kami akan mencakup, tetapi tidak terbatas pada: proses membangun tabel simbol dan membuat objek kode, objek Python, objek frame, objek kode, objek fungsional, opcodes, loop interpreter, generator, dan kelas pengguna.

Materi ini ditujukan bagi siapa saja yang tertarik mempelajari cara kerja mesin virtual CPython. Diasumsikan bahwa pengguna sudah terbiasa dengan python dan memahami dasar-dasar bahasa. Ketika mempelajari struktur mesin virtual, kita akan menemukan sejumlah besar kode-C, sehingga akan lebih mudah bagi pengguna yang memiliki pemahaman dasar bahasa C untuk memahami materi. Jadi, pada dasarnya, apa yang Anda butuhkan untuk berkenalan dengan materi ini: keinginan untuk mempelajari lebih lanjut tentang mesin virtual CPython.

Artikel ini adalah versi tambahan dari catatan pribadi yang dibuat dalam studi pekerjaan internal penerjemah. Ada banyak hal berkualitas dalam video PyCon , kuliah sekolah, dan blog ini.. Pekerjaan saya tidak akan selesai tanpa sumber pengetahuan yang fantastis ini.

Di akhir buku ini, pembaca akan dapat memahami seluk-beluk bagaimana interpreter Python menjalankan program Anda. Ini termasuk berbagai tahapan pelaksanaan program dan struktur data yang sangat penting dalam program. Untuk memulainya, kita akan melihat apa yang terjadi ketika program sepele dijalankan, ketika nama modul diteruskan ke penerjemah pada baris perintah. Kode yang dapat dieksekusi CPython dapat diinstal dari sumber, mengikuti Panduan Pengembang Python .

Buku ini menggunakan versi Python 3.

Tampilan 30.000 kaki


Bab ini berbicara tentang bagaimana penerjemah mengeksekusi program Python. Dalam bab-bab berikut, kami akan memeriksa berbagai bagian dari "teka-teki" ini dan memberikan deskripsi yang lebih rinci dari setiap bagian. Terlepas dari kompleksitas program yang ditulis dengan Python, proses ini selalu sama. Penjelasan luar biasa yang diberikan oleh Yaniv Aknin dalam serangkaian artikelnya tentang Python Internal menetapkan topik untuk diskusi kita.

Modul source test.py dapat dieksekusi dari baris perintah (ketika meneruskannya sebagai argumen ke program interpreter Python dalam bentuk $ python test.py). Ini hanyalah salah satu cara untuk memanggil executable Python. Kami juga dapat meluncurkan juru bahasa interaktif, mengeksekusi baris file sebagai kode, dll. Tapi ini dan metode lain tidak menarik bagi kita. Ini adalah transfer modul sebagai argumen (di dalam baris perintah) ke file yang dapat dieksekusi (Gambar 2.1) yang paling mencerminkan aliran berbagai tindakan yang terlibat dalam pelaksanaan kode yang sebenarnya.


Gambar 2.1: Streaming saat runtime.

Eksekusi python adalah program C biasa, jadi ketika dipanggil, proses yang serupa dengan yang ada, misalnya, di kernel linux atau program hello world yang sederhana, terjadi. Luangkan waktu sebentar untuk memahami: eksekusi python hanyalah program lain yang meluncurkan program Anda sendiri. "Hubungan" semacam itu ada antara bahasa C dan assembler (atau llvm). Proses inisialisasi standar (yang tergantung pada platform tempat eksekusi berlangsung) dimulai ketika python executable dipanggil dengan nama modul sebagai argumen.

Artikel ini mengasumsikan Anda menggunakan sistem operasi berbasis Unix, sehingga beberapa fitur mungkin berbeda pada Windows.

Bahasa C pada saat startup menjalankan semua "keajaiban" inisialisasi - memuat pustaka, memeriksa / menetapkan variabel lingkungan, dan setelah itu, metode utama dari python executable diluncurkan sama seperti program C lainnya. File executable utama Python terletak di ./Programs / python.c dan melakukan beberapa inisialisasi (seperti membuat salinan argumen baris perintah program yang diteruskan ke modul). Fungsi utama kemudian memanggil fungsi Py_Main yang terletak di ./Modules/main.c . Ia memproses proses inisialisasi interpreter: menganalisis argumen baris perintah, menetapkan flag, membaca variabel lingkungan, mengeksekusi kait, mengacak fungsi hash, dll. Disebut jugaPy_Initialize dari pylifecycle.c , yang menangani inisialisasi struktur data interpreter dan stream state, adalah dua struktur data yang sangat penting.

Meneliti deklarasi struktur data interpreter dan status streaming memperjelas mengapa mereka diperlukan. Keadaan interpreter dan stream hanyalah struktur dengan pointer ke bidang yang berisi informasi yang diperlukan untuk menjalankan program. Data status juru dibuat melalui typedef (anggap saja kata kunci ini dalam C sebagai definisi jenis, meskipun ini tidak sepenuhnya benar). Kode untuk struktur ini ditunjukkan pada Listing 2.1.

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

Daftar Kode 2.1: Struktur data status

juru bahasa Siapa pun yang telah menggunakan bahasa pemrograman Python sejak lama dapat mengenali beberapa bidang yang disebutkan dalam struktur ini (sysdict, builtins, codec).

  1. Kolom * next adalah referensi ke instance lain dari interpreter, karena beberapa interpreter Python dapat ada dalam proses yang sama.
  2. Bidang * tstate_head menunjukkan utas eksekusi utama (jika program multi-utas, maka penerjemah umum untuk semua utas yang dibuat oleh program). Kami akan membahas ini secara lebih rinci dalam waktu dekat.
  3. modules, modules_by_index, sysdict, builtins dan importlib berbicara sendiri. Semuanya didefinisikan sebagai instance dari PyObject , yang merupakan tipe root untuk semua objek di mesin virtual Python. Objek Python akan dibahas lebih rinci dalam bab-bab berikut.
  4. Bidang yang terkait dengan codec * berisi informasi yang membantu mengunduh pengodean. Ini sangat penting untuk decoding byte.

Eksekusi program harus terjadi di utas. Struktur keadaan aliran berisi semua informasi yang diperlukan aliran untuk menjalankan beberapa objek kode. Bagian dari struktur data aliran ditunjukkan pada Listing 2.2.

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

Listing 2.2: Bagian dari

struktur data state stream Struktur data interpreter dan state stream dibahas lebih rinci dalam bab-bab berikut. Proses inisialisasi juga menetapkan mekanisme impor serta stdio dasar.

Setelah menyelesaikan semua inisialisasi, Py_Main memanggil fungsi run_file (juga terletak di modul main.c). Berikut ini adalah serangkaian panggilan fungsi: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagsmenciptakan namespace __main__ di mana konten file akan dieksekusi. Ini juga memeriksa apakah versi file pyc ada (file pyc adalah file sederhana yang berisi versi kode sumber yang sudah dikompilasi). Jika versi pyc ada, upaya akan dilakukan untuk membacanya sebagai file biner, dan kemudian jalankan. Jika file pyc tidak ada, PyRun_FileExFlags dipanggil, dll. Fungsi PyParser_ASTFromFileObject memanggil PyParser_ParseFileObject , yang membaca isi modul dan membuat parsing tree dari itu. Kemudian, pohon yang dibuat diteruskan ke PyParser_ASTFromNodeObject , yang membuat pohon sintaksis abstrak darinya.

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

AST dihasilkan ketika run_mod dipanggil . Fungsi ini memanggil PyAST_CompileObject , yang membuat objek kode dari AST. Perhatikan bahwa bytecode yang dihasilkan selama panggilan PyAST_CompileObject dilewatkan melalui optimizer lubang intip , yang melakukan optimalisasi rendah bytecode yang dihasilkan sebelum membuat objek kode. Fungsi run_mod kemudian menerapkan fungsi PyEval_EvalCode dari file ceval.c ke objek kode. Ini mengarah ke serangkaian panggilan fungsi lain: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. Objek kode dilewatkan sebagai argumen ke sebagian besar fungsi ini dalam satu bentuk atau lainnya. _PyEval_EvalFrameEx- Ini adalah loop juru bahasa normal yang menangani eksekusi objek kode. Namun, ini disebut tidak hanya dengan objek kode sebagai argumen, tetapi dengan objek bingkai, yang memiliki atribut sebagai bidang yang merujuk ke objek kode. Bingkai ini menyediakan konteks untuk eksekusi objek kode. Dengan kata sederhana: loop juru terus membaca instruksi selanjutnya yang ditunjukkan oleh penghitung instruksi dari array instruksi. Kemudian ia mengeksekusi instruksi ini: ia menambah atau menghapus objek dari tumpukan nilai dalam proses sampai kosong ke dalam array instruksi yang akan dieksekusi (baik, atau sesuatu yang luar biasa terjadi yang mengganggu loop).

Python menyediakan satu set fungsi yang dapat Anda gunakan untuk memeriksa objek kode nyata. Misalnya, program sederhana dapat dikompilasi menjadi objek kode dan dibongkar untuk mendapatkan opcodes yang dieksekusi oleh mesin virtual python. Ini ditunjukkan pada Listing 2.3.

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

Daftar Kode 2.3: Membongkar fungsi dengan Python File

header ./Include/opcodes.h berisi daftar lengkap semua instruksi / opcode untuk mesin virtual Python. Opcode cukup sederhana. Ambil contoh kami di Listing 2.3, yang memiliki empat instruksi. LOAD_FAST memuat nilai argumennya (dalam hal ini x) ke tumpukan nilai. Mesin virtual python berbasis stack, sehingga nilai untuk operasi opcode "muncul" dari stack, dan hasil perhitungan didorong kembali ke stack untuk digunakan lebih lanjut oleh opcode lain. Kemudian BINARY_MULTIPLY muncul dua item dari stack, melakukan multiplikasi biner dari kedua nilai, dan mendorong hasilnya kembali ke stack. Instruksi NILAI PENGEMBALIANmengambil nilai dari tumpukan, menetapkan nilai kembali untuk objek ke nilai ini, dan keluar dari loop juru bahasa. Jika Anda melihat Listing 2.3, jelas bahwa ini adalah penyederhanaan yang cukup kuat.

Penjelasan saat ini dari loop juru tidak memperhitungkan sejumlah detail, yang akan dibahas dalam bab-bab berikutnya. Misalnya, berikut adalah pertanyaan yang kami tidak terima jawabannya:

  • Dari mana nilai-nilai yang dimuat oleh pernyataan LOAD_FAST berasal?
  • Dari mana datangnya argumen, yang digunakan sebagai bagian dari instruksi?
  • Bagaimana cara menangani fungsi dan metode bersarang ditangani?
  • Bagaimana loop interpreter menangani pengecualian?

Setelah menyelesaikan semua instruksi, fungsi Py_Main melanjutkan eksekusi, tetapi kali ini memulai proses pembersihan. Jika Py_Initialize dipanggil untuk melakukan inisialisasi selama dimulainya penerjemah, maka Py_FinalizeEx dipanggil untuk melakukan pembersihan. Proses ini termasuk menunggu keluar dari utas, memanggil semua penangan keluar, serta membebaskan memori yang masih digunakan yang dialokasikan oleh juru bahasa.

Jadi, kami melihat deskripsi "tingkat tinggi" dari proses yang terjadi di Python yang dapat dieksekusi ketika skrip dijalankan. Seperti disebutkan sebelumnya, ada banyak pertanyaan yang masih harus dijawab. Di masa depan, kami akan mempelajari studi juru bahasa dan mempertimbangkan secara rinci setiap tahap. Dan kita akan mulai dengan menjelaskan proses kompilasi pada bab selanjutnya.

All Articles