Bạn có thể thấy điều này hữu ích - Nội bộ Python: thêm một câu lệnh mới vào Python , được trích dẫn ở đây:
Bài viết này là một nỗ lực để hiểu rõ hơn về cách thức hoạt động của Python. Chỉ đọc tài liệu và mã nguồn có thể hơi nhàm chán, vì vậy tôi đang thực hiện một cách tiếp cận thực hành ở đây: Tôi sẽ thêm một until
tuyên bố vào Python.
Tất cả các mã hóa cho bài viết này đã được thực hiện đối với nhánh Py3k tiên tiến trong máy nhân bản kho lưu trữ Python Mercurial .
các until
tuyên bố
Một số ngôn ngữ, như Ruby, có một until
tuyên bố, đó là phần bổ sung cho while
( until num == 0
tương đương với while num != 0
). Trong Ruby, tôi có thể viết:
num = 3
until num == 0 do
puts num
num -= 1
end
Và nó sẽ in:
3
2
1
Vì vậy, tôi muốn thêm một khả năng tương tự với Python. Đó là, có thể viết:
num = 3
until num == 0:
print(num)
num -= 1
Một cuộc cải cách ngôn ngữ
Bài viết này không cố gắng đề xuất bổ sung một until
tuyên bố cho Python. Mặc dù tôi nghĩ rằng một tuyên bố như vậy sẽ làm cho một số mã rõ ràng hơn và bài viết này cho thấy việc thêm nó dễ dàng như thế nào, tôi hoàn toàn tôn trọng triết lý tối giản của Python. Tất cả những gì tôi đang cố gắng làm ở đây, thực sự, là hiểu rõ hơn về hoạt động bên trong của Python.
Sửa đổi ngữ pháp
Python sử dụng một trình tạo trình phân tích cú pháp tùy chỉnh có tên pgen
. Đây là trình phân tích cú pháp LL (1) chuyển đổi mã nguồn Python thành cây phân tích cú pháp. Đầu vào của trình tạo bộ phân tích cú pháp là tệp Grammar/Grammar
[1] . Đây là một tệp văn bản đơn giản chỉ định ngữ pháp của Python.
[1] : Từ đây trở đi, các tham chiếu đến các tệp trong nguồn Python được cung cấp tương đối cho thư mục gốc của cây nguồn, đây là thư mục nơi bạn chạy cấu hình và thực hiện để xây dựng Python.
Hai sửa đổi phải được thực hiện cho tập tin ngữ pháp. Đầu tiên là thêm một định nghĩa cho until
câu lệnh. Tôi tìm thấy nơi while
câu lệnh được định nghĩa ( while_stmt
) và được thêm vào until_stmt
bên dưới [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] : Điều này thể hiện một kỹ thuật phổ biến tôi sử dụng khi sửa đổi mã nguồn mà tôi không quen thuộc: làm việc bằng cách tương tự . Nguyên tắc này sẽ không giải quyết tất cả các vấn đề của bạn, nhưng nó chắc chắn có thể làm giảm quá trình. Vì tất cả mọi thứ phải được thực hiện while
cũng phải được thực hiện until
, nó phục vụ như một hướng dẫn khá tốt.
Lưu ý rằng tôi đã quyết định loại trừ else
mệnh đề khỏi định nghĩa của tôi until
, chỉ để làm cho nó khác đi một chút (và thật lòng tôi không thích else
mệnh đề của các vòng lặp và không nghĩ rằng nó phù hợp với Zen of Python).
Thay đổi thứ hai là sửa đổi quy tắc cho compound_stmt
để bao gồm until_stmt
, như bạn có thể thấy trong đoạn trích ở trên. Đó là ngay sau đó while_stmt
, một lần nữa.
Khi bạn chạy make
sau khi sửa đổi Grammar/Grammar
, hãy chú ý rằngpgen
chương trình đang chạy để tái tạo Include/graminit.h
và Python/graminit.c
, và sau đó một số tác phẩm được tái biên dịch.
Sửa đổi mã tạo AST
Sau khi trình phân tích cú pháp Python đã tạo một cây phân tích cú pháp, cây này được chuyển đổi thành AST, vì AST là đơn giản hơn nhiều để làm việc với các giai đoạn tiếp theo của quá trình biên dịch.
Vì vậy, chúng tôi sẽ truy cập vào Parser/Python.asdl
đó xác định cấu trúc AST của Python và thêm nút AST cho until
câu lệnh mới của chúng tôi , một lần nữa ngay bên dưới while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Nếu bây giờ bạn chạy make
, hãy lưu ý rằng trước khi biên dịch một loạt các tệp, Parser/asdl_c.py
được chạy để tạo mã C từ tệp định nghĩa AST. Đây (như Grammar/Grammar
) là một ví dụ khác về mã nguồn Python sử dụng ngôn ngữ nhỏ (nói cách khác là DSL) để đơn giản hóa việc lập trình. Cũng lưu ý rằng vì Parser/asdl_c.py
là tập lệnh Python, đây là một kiểu bootstrapping - để xây dựng Python từ đầu, Python đã có sẵn.
Trong khi Parser/asdl_c.py
tạo mã để quản lý nút AST mới được xác định của chúng tôi (vào các tệp Include/Python-ast.h
và Python/Python-ast.c
), chúng tôi vẫn phải viết mã chuyển đổi nút phân tích cú pháp cây có liên quan thành nút bằng tay. Điều này được thực hiện trong tập tin Python/ast.c
. Ở đó, một hàm có tên là ast_for_stmt
chuyển đổi các nút cây phân tích cú pháp cho các câu lệnh thành các nút AST. Một lần nữa, được hướng dẫn bởi người bạn cũ của chúng tôi while
, chúng tôi nhảy ngay vào lớn switch
để xử lý các câu lệnh ghép và thêm một mệnh đề cho until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Bây giờ chúng ta nên thực hiện ast_for_until_stmt
. Đây là:
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;
}
Một lần nữa, điều này đã được mã hóa trong khi xem xét tương đương ast_for_while_stmt
, với sự khác biệt là until
tôi đã quyết định không hỗ trợ else
điều khoản này. Như mong đợi, AST được tạo đệ quy, sử dụng các hàm tạo AST khác như ast_for_expr
cho biểu thức điều kiện và ast_for_suite
cho phần thân của until
câu lệnh. Cuối cùng, một nút mới có tênUntil
được trả về.
Lưu ý rằng chúng tôi truy cập nút phân tích cú pháp n
bằng cách sử dụng một số macro như NCH
và CHILD
. Đây là những giá trị hiểu biết - mã của họ là trongInclude/node.h
.
Digression: Thành phần AST
Tôi đã chọn tạo một loại AST mới cho until
tuyên bố, nhưng thực sự điều này không cần thiết. Tôi đã có thể lưu một số công việc và triển khai chức năng mới bằng cách sử dụng thành phần của các nút AST hiện có, kể từ:
until condition:
# do stuff
Có chức năng tương đương với:
while not condition:
# do stuff
Thay vì tạo Until
nút trong ast_for_until_stmt
, tôi có thể đã tạo một Not
nút có While
nút khi còn nhỏ. Vì trình biên dịch AST đã biết cách xử lý các nút này, các bước tiếp theo của quy trình có thể được bỏ qua.
Biên dịch AST thành mã byte
Bước tiếp theo là biên dịch AST thành mã byte Python. Quá trình biên dịch có kết quả trung gian là CFG (Control Flow Graph), nhưng vì cùng một mã xử lý nên tôi sẽ bỏ qua chi tiết này ngay bây giờ và để lại cho một bài viết khác.
Mã chúng ta sẽ xem xét tiếp theo là Python/compile.c
. Theo sự dẫn dắt củawhile
, chúng tôi tìm thấy hàm compiler_visit_stmt
chịu trách nhiệm biên dịch các câu lệnh thành mã byte. Chúng tôi thêm một điều khoản cho Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Nếu bạn tự hỏi nó Until_kind
là gì , đó là một hằng số (thực sự là một giá trị của phép _stmt_kind
liệt kê) tự động được tạo từ tệp định nghĩa AST vào Include/Python-ast.h
. Dù sao, chúng tôi gọicompiler_until
đó, tất nhiên, vẫn không tồn tại. Tôi sẽ đến đó một lát.
Nếu bạn tò mò như tôi, bạn sẽ nhận thấy điều đó compiler_visit_stmt
thật đặc biệt. Không có số lượng grep
-ping cây nguồn cho thấy nơi nó được gọi. Khi gặp trường hợp này, chỉ còn một tùy chọn - C macro-fu. Thật vậy, một cuộc điều tra ngắn dẫn chúng ta đến VISIT
vĩ mô được xác định trong Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Nó được sử dụng để gọi compiler_visit_stmt
trongcompiler_body
. Quay lại với công việc của chúng tôi, tuy nhiên ...
Như đã hứa, đây 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;
}
Tôi có một lời thú nhận để thực hiện: mã này không được viết dựa trên sự hiểu biết sâu sắc về mã byte Python. Giống như phần còn lại của bài viết, nó đã được thực hiện trong việc bắt chước compiler_while
chức năng kin . Tuy nhiên, bằng cách đọc nó một cách cẩn thận, hãy nhớ rằng Python VM dựa trên stack và liếc vào tài liệu củadis
mô-đun, trong đó có một danh sách các mã byte Python với các mô tả, có thể hiểu những gì đang diễn ra.
Thế là xong, chúng ta đã xong ... Phải không?
Sau khi thực hiện tất cả các thay đổi và chạy make
, chúng ta có thể chạy Python mới được biên dịch và thử until
tuyên bố mới của chúng tôi :
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, nó hoạt động! Chúng ta hãy xem mã byte được tạo cho câu lệnh mới bằng cách sử dụng dis
mô-đun như sau:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Đây là kết quả:
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
Hoạt động thú vị nhất là số 12: nếu điều kiện là đúng, chúng ta nhảy tới sau vòng lặp. Đây là ngữ nghĩa chính xác chountil
. Nếu bước nhảy không được thực thi, thân vòng lặp sẽ tiếp tục chạy cho đến khi nó nhảy trở lại điều kiện khi hoạt động 35.
Cảm thấy tốt về sự thay đổi của mình, sau đó tôi đã thử chạy chức năng (thực thi myfoo(3)
) thay vì hiển thị mã byte của nó. Kết quả không đáng khích lệ:
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 ... điều này không thể tốt được. Vì vậy, những gì đã đi sai?
Trường hợp bảng biểu tượng bị thiếu
Một trong những bước mà trình biên dịch Python thực hiện khi biên dịch AST là tạo bảng ký hiệu cho mã mà nó biên dịch. Cuộc gọi đến PySymtable_Build
trong PyAST_Compile
các cuộc gọi vào mô-đun bảng biểu tượng (Python/symtable.c
), đi theo AST theo cách tương tự như các hàm tạo mã. Có một bảng ký hiệu cho mỗi phạm vi giúp trình biên dịch tìm ra một số thông tin chính, chẳng hạn như biến nào là toàn cục và biến cục bộ trong phạm vi.
Để khắc phục sự cố, chúng tôi phải sửa đổi symtable_visit_stmt
hàm trong Python/symtable.c
, thêm mã để xử lý các until
câu lệnh, sau mã tương tự cho các while
câu lệnh [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] : Nhân tiện, không có mã này, có một cảnh báo về trình biên dịch Python/symtable.c
. Trình biên dịch thông báo rằng Until_kind
giá trị liệt kê không được xử lý trong câu lệnh chuyển đổi củasymtable_visit_stmt
và phàn nàn. Nó luôn luôn quan trọng để kiểm tra các cảnh báo trình biên dịch!
Và bây giờ chúng tôi thực sự đã hoàn thành. Biên dịch nguồn sau khi thay đổi này làm cho việc thực hiện myfoo(3)
công việc như mong đợi.
Phần kết luận
Trong bài viết này, tôi đã trình bày cách thêm một câu lệnh mới vào Python. Mặc dù đòi hỏi khá nhiều sự mày mò trong mã của trình biên dịch Python, sự thay đổi không khó thực hiện, vì tôi đã sử dụng một câu lệnh tương tự và hiện có làm hướng dẫn.
Trình biên dịch Python là một phần mềm tinh vi và tôi không khẳng định mình là một chuyên gia về nó. Tuy nhiên, tôi thực sự quan tâm đến phần bên trong của Python và đặc biệt là phần đầu của nó. Do đó, tôi thấy bài tập này là một người bạn đồng hành rất hữu ích để nghiên cứu lý thuyết về các nguyên tắc và mã nguồn của trình biên dịch. Nó sẽ phục vụ như là một cơ sở cho các bài viết trong tương lai sẽ đi sâu hơn vào trình biên dịch.
Người giới thiệu
Tôi đã sử dụng một vài tài liệu tham khảo tuyệt vời cho việc xây dựng bài viết này. Ở đây họ không theo thứ tự đặc biệt:
- PEP 339: Thiết kế trình biên dịch CPython - có lẽ là phần tài liệu chính thức quan trọng và toàn diện nhất cho trình biên dịch Python. Rất ngắn, nó đau đớn hiển thị sự khan hiếm tài liệu tốt về các phần bên trong của Python.
- "Python Compiler Internals" - một bài viết của Thomas Lee
- "Python: Thiết kế và triển khai" - một bài thuyết trình của Guido van Rossum
- Máy ảo Python (2.5), Chuyến tham quan có hướng dẫn - bài thuyết trình của Peter Tröger
nguồn chính thức