Anda mungkin menemukan ini berguna - internal Python: menambahkan pernyataan baru ke Python , dikutip di sini:
Artikel ini adalah upaya untuk lebih memahami cara kerja front-end Python. Hanya membaca dokumentasi dan kode sumber mungkin sedikit membosankan, jadi saya melakukan pendekatan langsung di sini: Saya akan menambahkan until
pernyataan ke Python.
Semua pengkodean untuk artikel ini dilakukan pada cabang Py3k yang mutakhir di cermin repositori Python Mercurial .
The until
pernyataan
Beberapa bahasa, seperti Ruby, memiliki until
pernyataan, yang merupakan pelengkap untuk while
( until num == 0
setara dengan while num != 0
). Di Ruby, saya bisa menulis:
num = 3
until num == 0 do
puts num
num -= 1
end
Dan itu akan mencetak:
3
2
1
Jadi, saya ingin menambahkan kemampuan yang mirip dengan Python. Artinya, mampu menulis:
num = 3
until num == 0:
print(num)
num -= 1
Penyimpangan advokasi bahasa
Artikel ini tidak mencoba menyarankan penambahan until
pernyataan ke Python. Meskipun menurut saya pernyataan seperti itu akan membuat beberapa kode lebih jelas, dan artikel ini menunjukkan betapa mudahnya menambahkannya, saya sepenuhnya menghormati filosofi minimalisme Python. Semua yang saya coba lakukan di sini, sungguh, adalah mendapatkan beberapa wawasan tentang cara kerja Python.
Mengubah tata bahasa
Python menggunakan generator parser khusus bernama pgen
. Ini adalah parser LL (1) yang mengubah kode sumber Python menjadi pohon parse. Input ke generator parser adalah file Grammar/Grammar
[1] . Ini adalah file teks sederhana yang menentukan tata bahasa Python.
[1] : Mulai saat ini, referensi ke file dalam sumber Python diberikan secara relatif ke akar dari pohon sumber, yang merupakan direktori tempat Anda menjalankan konfigurasi dan membuat untuk membangun Python.
Dua modifikasi harus dilakukan pada file tata bahasa. Yang pertama adalah menambahkan definisi untuk until
pernyataan tersebut. Saya menemukan di mana while
pernyataan itu didefinisikan ( while_stmt
), dan ditambahkan di until_stmt
bawah [2] :
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite
[2] : Ini mendemonstrasikan teknik umum yang saya gunakan saat memodifikasi kode sumber yang tidak saya kenal: bekerja berdasarkan kesamaan . Prinsip ini tidak akan menyelesaikan semua masalah Anda, tetapi pasti dapat mempermudah prosesnya. Karena segala sesuatu yang harus diselesaikan while
juga harus diselesaikan until
, itu berfungsi sebagai pedoman yang cukup baik.
Perhatikan bahwa saya telah memutuskan untuk mengecualikan else
klausa dari definisi saya until
, hanya untuk membuatnya sedikit berbeda (dan karena terus terang saya tidak menyukai else
klausa loop dan tidak merasa klausa itu cocok dengan Zen of Python).
Perubahan kedua adalah mengubah aturan untuk compound_stmt
disertakan until_stmt
, seperti yang Anda lihat pada cuplikan di atas. Ini tepat setelahnya while_stmt
, lagi.
Saat Anda menjalankan make
setelah memodifikasi Grammar/Grammar
, perhatikan bahwa pgen
program dijalankan untuk menghasilkan ulang Include/graminit.h
dan Python/graminit.c
, kemudian beberapa file akan dikompilasi ulang.
Memodifikasi kode generasi AST
Setelah Python parser membuat pohon parse, pohon ini diubah menjadi AST, karena AST jauh lebih sederhana untuk dikerjakan pada tahap selanjutnya dari proses kompilasi.
Jadi, kita akan mengunjungi Parser/Python.asdl
yang mendefinisikan struktur AST Python dan menambahkan simpul AST untuk until
pernyataan baru kita , lagi tepat di bawah while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Jika Anda sekarang menjalankan make
, perhatikan bahwa sebelum mengkompilasi banyak file, Parser/asdl_c.py
dijalankan untuk menghasilkan kode C dari file definisi AST. Ini (suka Grammar/Grammar
) adalah contoh lain dari kode sumber Python yang menggunakan bahasa mini (dengan kata lain, DSL) untuk menyederhanakan pemrograman. Perhatikan juga bahwa karena Parser/asdl_c.py
ini adalah skrip Python, ini adalah sejenis bootstrap - untuk membuat Python dari awal, Python harus tersedia.
Saat Parser/asdl_c.py
membuat kode untuk mengelola node AST yang baru kami definisikan (ke dalam file Include/Python-ast.h
dan Python/Python-ast.c
), kami masih harus menulis kode yang mengubah node parse-tree yang relevan ke dalamnya dengan tangan. Ini dilakukan di file Python/ast.c
. Di sana, fungsi bernama ast_for_stmt
mengubah simpul pohon parse untuk pernyataan menjadi simpul AST. Sekali lagi, dipandu oleh teman lama while
kita, kita langsung terjun ke masalah besar switch
untuk menangani pernyataan majemuk dan menambahkan klausa untuk until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Sekarang kita harus menerapkan ast_for_until_stmt
. Ini dia:
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
/* until_stmt: 'until' test ':' suite */
REQ(n, until_stmt);
if (NCH(n) == 4) {
expr_ty expression;
asdl_seq *suite_seq;
expression = ast_for_expr(c, CHILD(n, 1));
if (!expression)
return NULL;
suite_seq = ast_for_suite(c, CHILD(n, 3));
if (!suite_seq)
return NULL;
return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
}
PyErr_Format(PyExc_SystemError,
"wrong number of tokens for 'until' statement: %d",
NCH(n));
return NULL;
}
Sekali lagi, ini diberi kode sambil melihat dari dekat padanannya ast_for_while_stmt
, dengan perbedaan karena until
saya telah memutuskan untuk tidak mendukung else
klausul tersebut. Seperti yang diharapkan, AST dibuat secara rekursif, menggunakan fungsi pembuatan AST lainnya seperti ast_for_expr
untuk ekspresi kondisi dan ast_for_suite
untuk isi until
pernyataan. Akhirnya, simpul baru bernama Until
dikembalikan.
Perhatikan bahwa kami mengakses simpul pohon parse n
menggunakan beberapa makro seperti NCH
dan CHILD
. Ini layak untuk dipahami - kode mereka ada di dalamnya Include/node.h
.
Pencernaan: Komposisi AST
Saya memilih untuk membuat jenis AST baru untuk until
pernyataan tersebut, tetapi sebenarnya ini tidak perlu. Saya bisa menyimpan beberapa pekerjaan dan menerapkan fungsi baru menggunakan komposisi node AST yang ada, karena:
until condition:
# do stuff
Secara fungsional setara dengan:
while not condition:
# do stuff
Alih-alih membuat Until
simpul di ast_for_until_stmt
, saya bisa membuat Not
simpul dengan While
simpul sebagai anak. Karena kompilator AST sudah mengetahui cara menangani node ini, langkah proses selanjutnya dapat dilewati.
Mengompilasi AST menjadi bytecode
Langkah selanjutnya adalah menyusun AST menjadi bytecode Python. Kompilasi memiliki hasil perantara yang merupakan CFG (Control Flow Graph), tetapi karena kode yang sama menanganinya, saya akan mengabaikan detail ini untuk saat ini dan membiarkannya untuk artikel lain.
Kode yang akan kita lihat selanjutnya adalah Python/compile.c
. Mengikuti petunjuk dari while
, kami menemukan fungsi compiler_visit_stmt
, yang bertanggung jawab untuk menyusun pernyataan menjadi bytecode. Kami menambahkan klausa untuk Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Jika Anda bertanya-tanya apa Until_kind
itu, itu adalah konstanta (sebenarnya nilai _stmt_kind
enumerasi) yang secara otomatis dihasilkan dari file definisi AST ke Include/Python-ast.h
. Bagaimanapun, kami menyebutnya compiler_until
yang, tentu saja, masih belum ada. Saya akan membahasnya sebentar.
Jika Anda penasaran seperti saya, Anda akan melihat itu compiler_visit_stmt
aneh. Tidak ada jumlah grep
-ping dari pohon sumber yang mengungkapkan di mana ia dipanggil. Jika demikian, hanya satu opsi yang tersisa - C makro-fu. Memang, investigasi singkat membawa kita ke VISIT
makro yang didefinisikan di Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Ini digunakan untuk memohon compiler_visit_stmt
di compiler_body
. Kembali ke bisnis kami, namun ...
Seperti yang dijanjikan, berikut ini compiler_until
:
static int
compiler_until(struct compiler *c, stmt_ty s)
{
basicblock *loop, *end, *anchor = NULL;
int constant = expr_constant(s->v.Until.test);
if (constant == 1) {
return 1;
}
loop = compiler_new_block(c);
end = compiler_new_block(c);
if (constant == -1) {
anchor = compiler_new_block(c);
if (anchor == NULL)
return 0;
}
if (loop == NULL || end == NULL)
return 0;
ADDOP_JREL(c, SETUP_LOOP, end);
compiler_use_next_block(c, loop);
if (!compiler_push_fblock(c, LOOP, loop))
return 0;
if (constant == -1) {
VISIT(c, expr, s->v.Until.test);
ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
}
VISIT_SEQ(c, stmt, s->v.Until.body);
ADDOP_JABS(c, JUMP_ABSOLUTE, loop);
if (constant == -1) {
compiler_use_next_block(c, anchor);
ADDOP(c, POP_BLOCK);
}
compiler_pop_fblock(c, LOOP, loop);
compiler_use_next_block(c, end);
return 1;
}
Saya harus membuat pengakuan: kode ini tidak ditulis berdasarkan pemahaman mendalam tentang bytecode Python. Seperti artikel lainnya, itu dilakukan dengan meniru compiler_while
fungsi kerabat . Dengan membacanya dengan hati-hati, bagaimanapun, dengan mengingat bahwa VM Python berbasis tumpukan, dan melihat sekilas ke dalam dokumentasi dis
modul, yang memiliki daftar bytecode Python dengan deskripsi, mungkin untuk memahami apa yang terjadi.
Itu saja, kita sudah selesai ... bukan?
Setelah membuat semua perubahan dan menjalankan make
, kita dapat menjalankan Python yang baru dikompilasi dan mencoba until
pernyataan baru kita :
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, berhasil! Mari kita lihat bytecode yang dibuat untuk pernyataan baru dengan menggunakan dis
modul sebagai berikut:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Inilah hasilnya:
4 0 SETUP_LOOP 36 (to 39)
>> 3 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (0)
9 COMPARE_OP 2 (==)
12 POP_JUMP_IF_TRUE 38
5 15 LOAD_NAME 0 (print)
18 LOAD_FAST 0 (num)
21 CALL_FUNCTION 1
24 POP_TOP
6 25 LOAD_FAST 0 (num)
28 LOAD_CONST 2 (1)
31 INPLACE_SUBTRACT
32 STORE_FAST 0 (num)
35 JUMP_ABSOLUTE 3
>> 38 POP_BLOCK
>> 39 LOAD_CONST 0 (None)
42 RETURN_VALUE
Operasi yang paling menarik adalah nomor 12: jika kondisinya benar, kita lompat ke setelah pengulangan. Ini adalah semantik yang benar untuk until
. Jika lompatan tidak dijalankan, badan loop terus berjalan hingga melompat kembali ke kondisi pada operasi 35.
Merasa senang dengan perubahan saya, saya kemudian mencoba menjalankan fungsi (mengeksekusi myfoo(3)
) alih-alih menampilkan bytecode-nya. Hasilnya kurang menggembirakan:
Traceback (most recent call last):
File "zy.py", line 9, in
myfoo(3)
File "zy.py", line 5, in myfoo
print(num)
SystemError: no locals when loading 'print'
Whoa ... ini tidak bagus. Jadi apa yang salah?
Kasus tabel simbol yang hilang
Salah satu langkah yang dilakukan oleh compiler Python saat mengompilasi AST adalah membuat tabel simbol untuk kode yang dikompilasinya. Panggilan ke PySymtable_Build
dalam PyAST_Compile
panggilan ke modul tabel simbol ( Python/symtable.c
), yang menjalankan AST dengan cara yang mirip dengan fungsi pembuatan kode. Memiliki tabel simbol untuk setiap ruang lingkup membantu kompilator menemukan beberapa informasi kunci, seperti variabel mana yang global dan yang lokal untuk suatu cakupan.
Untuk memperbaiki masalah ini, kita harus memodifikasi symtable_visit_stmt
fungsi in Python/symtable.c
, menambahkan kode untuk menangani until
pernyataan, setelah kode serupa untuk while
pernyataan [3] :
case While_kind:
VISIT(st, expr, s->v.While.test);
VISIT_SEQ(st, stmt, s->v.While.body);
if (s->v.While.orelse)
VISIT_SEQ(st, stmt, s->v.While.orelse);
break;
case Until_kind:
VISIT(st, expr, s->v.Until.test);
VISIT_SEQ(st, stmt, s->v.Until.body);
break;
[3] : Omong-omong, tanpa kode ini ada peringatan compiler untuk Python/symtable.c
. Kompilator memperhatikan bahwa nilai Until_kind
enumerasi tidak ditangani dalam pernyataan switch symtable_visit_stmt
dan komplain. Selalu penting untuk memeriksa peringatan kompiler!
Dan sekarang kita benar-benar selesai. Mengompilasi sumber setelah perubahan ini membuat eksekusi myfoo(3)
pekerjaan seperti yang diharapkan.
Kesimpulan
Dalam artikel ini saya telah mendemonstrasikan cara menambahkan pernyataan baru ke Python. Meskipun membutuhkan sedikit perubahan dalam kode kompiler Python, perubahan itu tidak sulit untuk diterapkan, karena saya menggunakan pernyataan yang serupa dan yang sudah ada sebagai pedoman.
Kompiler Python adalah perangkat lunak yang canggih, dan saya tidak mengklaim sebagai ahli di dalamnya. Namun, saya sangat tertarik dengan bagian dalam Python, dan khususnya bagian depannya. Oleh karena itu, saya menemukan latihan ini sebagai pendamping yang sangat berguna untuk studi teoritis dari prinsip-prinsip kompiler dan kode sumber. Ini akan berfungsi sebagai dasar untuk artikel mendatang yang akan membahas lebih dalam kompiler.
Referensi
Saya menggunakan beberapa referensi bagus untuk konstruksi artikel ini. Di sini mereka tanpa urutan tertentu:
- PEP 339: Desain kompilator CPython - mungkin bagian paling penting dan komprehensif dari dokumentasi resmi untuk kompilator Python. Singkatnya, ini dengan menyakitkan menampilkan kelangkaan dokumentasi yang baik dari internal Python.
- "Python Compiler Internals" - artikel oleh Thomas Lee
- "Python: Desain dan Implementasi" - presentasi oleh Guido van Rossum
- Mesin Virtual Python (2.5), Tur terpandu - presentasi oleh Peter Tröger
sumber asli