2013-03-02 25 views
39

Sau khi đi sâu vào mã nguồn của Python, tôi phát hiện ra rằng nó duy trì một mảng khác nhau từ một (01) đến int (256) nó:Có gì với bộ đệm ẩn trong Python?

>>> a = 1 
>>> b = 1 
>>> a is b 
True 
>>> a = 257 
>>> b = 257 
>>> a is b 
False 

Nhưng nếu tôi chạy những đang cùng nhau trong một file py (hoặc tham gia cùng họ với dấu chấm phẩy), kết quả là khác nhau:

>>> a = 257; b = 257; a is b 
True 

tôi tò mò tại sao họ vẫn còn cùng một đối tượng, vì vậy tôi digg sâu hơn vào cây cú pháp và comp iler, tôi đã đưa ra một hệ thống phân cấp gọi điện thoại được liệt kê dưới đây:

PyRun_FileExFlags() 
    mod = PyParser_ASTFromFile() 
     node *n = PyParser_ParseFileFlagsEx() //source to cst 
      parsetoke() 
       ps = PyParser_New() 
       for (;;) 
        PyTokenizer_Get() 
        PyParser_AddToken(ps, ...) 
     mod = PyAST_FromNode(n, ...) //cst to ast 
    run_mod(mod, ...) 
     co = PyAST_Compile(mod, ...) //ast to CFG 
      PyFuture_FromAST() 
      PySymtable_Build() 
      co = compiler_mod() 
     PyEval_EvalCode(co, ...) 
      PyEval_EvalCodeEx() 

Sau đó, tôi đã thêm một số mã debug trong PyInt_FromLong và trước/sau PyAST_FromNode, và thực hiện một test.py:

a = 257 
b = 257 
print "id(a) = %d, id(b) = %d" % (id(a), id(b)) 

cái nhìn đầu ra như:

DEBUG: before PyAST_FromNode 
name = a 
ival = 257, id = 176046536 
name = b 
ival = 257, id = 176046752 
name = a 
name = b 
DEBUG: after PyAST_FromNode 
run_mod 
PyAST_Compile ok 
id(a) = 176046536, id(b) = 176046536 
Eval ok 

Nó có nghĩa rằng trong cst-ast biến đổi, hai khác nhau PyInt_Object s được tạo ra (thực tế nó được thực hiện trong hàm ast_for_atom()), nhưng sau đó chúng được hợp nhất.

Tôi thấy khó hiểu nguồn ở số PyAST_CompilePyEval_EvalCode, vì vậy tôi ở đây để yêu cầu trợ giúp, tôi sẽ đánh giá cao nếu một số gợi ý?

+1

Bạn chỉ đang cố gắng hiểu nguồn Python hoạt động như thế nào, hoặc bạn đang cố gắng hiểu những gì upshot dành cho mã được viết bằng Python? Bởi vì upshot cho mã được viết bằng Python là "đây là một chi tiết thực hiện, không bao giờ dựa vào nó xảy ra hoặc không xảy ra". – BrenBarn

+0

Tôi sẽ không dựa vào chi tiết triển khai. Tôi chỉ tò mò và cố gắng đột nhập vào mã nguồn. – felix021

+0

Liên quan: [Python "là" toán tử hoạt động bất ngờ với số nguyên] (http://stackoverflow.com/questions/306313/python-is-operator-behaves-unexpectedly-with-integers) – Blckknght

Trả lời

49

Python lưu trữ các số nguyên trong phạm vi [-5, 256], vì vậy, các số nguyên trong phạm vi đó cũng giống hệt nhau.

Những gì bạn thấy là trình biên dịch Python tối ưu hóa các chữ giống hệt nhau khi một phần của cùng một văn bản.

Khi gõ vào Python shell mỗi dòng là một tuyên bố hoàn toàn khác nhau, phân tích cú pháp trong một thời điểm khác nhau, do đó:

>>> a = 257 
>>> b = 257 
>>> a is b 
False 

Nhưng nếu bạn đặt cùng mã vào một tập tin:

$ echo 'a = 257 
> b = 257 
> print a is b' > testing.py 
$ python testing.py 
True 

Điều này xảy ra bất cứ khi nào trình phân tích cú pháp có cơ hội phân tích nơi sử dụng các chữ cái, ví dụ: khi xác định hàm trong trình thông dịch tương tác:

>>> def test(): 
...  a = 257 
...  b = 257 
...  print a is b 
... 
>>> dis.dis(test) 
    2   0 LOAD_CONST    1 (257) 
       3 STORE_FAST    0 (a) 

    3   6 LOAD_CONST    1 (257) 
       9 STORE_FAST    1 (b) 

    4   12 LOAD_FAST    0 (a) 
      15 LOAD_FAST    1 (b) 
      18 COMPARE_OP    8 (is) 
      21 PRINT_ITEM   
      22 PRINT_NEWLINE  
      23 LOAD_CONST    0 (None) 
      26 RETURN_VALUE   
>>> test() 
True 
>>> test.func_code.co_consts 
(None, 257) 

Lưu ý cách mã được biên dịch chứa một hằng số cho 257.

Tóm lại, trình biên dịch bytecode của Python không thể thực hiện tối ưu hóa lớn (như các ngôn ngữ kiểu tĩnh), nhưng nó có nhiều hơn bạn nghĩ. Một trong những điều này là phân tích việc sử dụng các chữ và tránh sao chép chúng.

Lưu ý rằng điều này không phải làm gì với bộ nhớ cache, vì nó hoạt động cũng cho nổi, mà không có một bộ nhớ cache:

>>> a = 5.0 
>>> b = 5.0 
>>> a is b 
False 
>>> a = 5.0; b = 5.0 
>>> a is b 
True 

Đối literals phức tạp hơn, như các bộ, nó "không làm việc ":

>>> a = (1,2) 
>>> b = (1,2) 
>>> a is b 
False 
>>> a = (1,2); b = (1,2) 
>>> a is b 
False 

Nhưng literals bên trong tuple được chia sẻ:

>>> a = (257, 258) 
>>> b = (257, 258) 
>>> a[0] is b[0] 
False 
>>> a[1] is b[1] 
False 
>>> a = (257, 258); b = (257, 258) 
>>> a[0] is b[0] 
True 
>>> a[1] is b[1] 
True 

Về lý do tại sao bạn thấy rằng hai số PyInt_Object được tạo, tôi muốn đoán rằng việc này được thực hiện để tránh so sánh theo nghĩa đen. ví dụ, số 257 có thể được thể hiện bằng nhiều literals:

>>> 257 
257 
>>> 0x101 
257 
>>> 0b100000001 
257 
>>> 0o401 
257 

Các phân tích cú pháp có hai lựa chọn:

  • Chuyển đổi các literals đối với một số cơ sở chung trước khi tạo các số nguyên, và xem nếu literals là tương đương. sau đó tạo một đối tượng số nguyên duy nhất.
  • Tạo các đối tượng số nguyên và xem chúng có bằng nhau hay không. Nếu có, chỉ giữ một giá trị duy nhất và gán nó cho tất cả các chữ, nếu không, bạn đã có số nguyên để gán.

Có lẽ trình phân tích cú pháp Python sử dụng phương pháp thứ hai, tránh viết lại mã chuyển đổi và cũng dễ mở rộng hơn (ví dụ: nó hoạt động với các phao).


Đọc file Python/ast.c, chức năng mà phân tích tất cả các số là parsenumber, trong đó kêu gọi PyOS_strtoul để có được những giá trị số nguyên (ví intgers) và cuối cùng gọi PyLong_FromString:

x = (long) PyOS_strtoul((char *)s, (char **)&end, 0); 
    if (x < 0 && errno == 0) { 
     return PyLong_FromString((char *)s, 
           (char **)0, 
           0); 
    } 

Như bạn có thể thấy ở đây trình phân tích cú pháp thực hiện không kiểm tra xem nó đã tìm thấy một số nguyên với giá trị đã cho hay chưa và do đó điều này giải thích tại sao bạn thấy rằng hai đối tượng int được tạo, và điều này cũng có nghĩa là dự đoán của tôi là chính xác: t tạo ra các hằng số và chỉ sau đó tối ưu hóa bytecode để sử dụng cùng một đối tượng cho các hằng số bằng nhau.

Mã kiểm tra này phải ở đâu đó trong Python/compile.c hoặc Python/peephole.c, vì đây là các tệp chuyển đổi AST thành bytecode.

Cụ thể, chức năng compiler_add_o có vẻ như là chức năng thực hiện. Có nhận xét này trong compiler_lambda:

/* Make None the first constant, so the lambda can't have a 
    docstring. */ 
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0) 
    return 0; 

Vì vậy, nó có vẻ như compiler_add_o được sử dụng để chèn các hằng số cho các chức năng/lambdas, vv Chức năng compiler_add_o lưu trữ các hằng số vào một đối tượng dict, và từ này ngay lập tức sau đó hằng số bằng sẽ rơi vào cùng một vị trí, dẫn đến một hằng số duy nhất trong bytecode cuối cùng.

+0

Cảm ơn. Tôi biết tại sao intepreter làm điều này, và tôi cũng đã thử nghiệm các chuỗi trước đây, nó hoạt động giống như int và float, và tôi cũng đã in cây cú pháp bằng cách sử dụng compiler.parse() cho thấy hai Const (257). Tôi chỉ tự hỏi khi nào và như thế nào trong mã nguồn ... Hơn nữa thử nghiệm tôi đã làm ở trên cho thấy rằng intepreter đã tạo ra hai PyInt_Object cho a và b, do đó, có rất ít ý nghĩa sáp nhập chúng (ngoài việc tiết kiệm bộ nhớ). – felix021

+0

@ felix021 Tôi đã cập nhật câu trả lời của mình một lần nữa. Tôi tìm thấy nơi hai int được tạo ra và tôi biết trong đó các tập tin tối ưu hóa xảy ra, mặc dù tôi vẫn không tìm thấy dòng chính xác của mã mà xử lý đó. – Bakuriu

+0

Cảm ơn rất nhiều! Tôi cẩn thận đi qua compile.c, chuỗi gọi là compiler_visit_stmt -> VISIT (c, expr, e) -> compiler_visit_expr (c, e) -> ADDOP_O (c, LOAD_CONST, e-> v.Num.n, consts) -> compiler_addop_o (c, LOAD_CONSTS, c-> u-> u_consts, e-> v.Num.n) -> compiler_add_o (c, c-> u-> u_consts, e-> v.Num.n). trong compoler_add_o(), python sẽ cố gắng dùng PyTuple (PyIntObject n, PyInt_Type) nếu gõ vào c-> u-> u_consts, và trong khi tính toán giá trị băm của tuple đó, chỉ int thực tế giá trị được sử dụng, vì vậy chỉ một PyInt_Object sẽ được chèn vào dict u_consts. – felix021

Các vấn đề liên quan