Python lưu trữ các số nguyên trong phạm vi [-5, 256]
, vì vậy dự kiến rằng 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 ký tự giống hệt nhau khi một phần của cùng một văn bản.
Khi nhập trong Python shell, mỗi dòng là một câu lệnh hoàn toàn khác nhau, được 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ột mã vào một tệp:
$ 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 các ký tự được sử dụng, ví dụ: khi xác định một 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ã đã biên dịch chứa một hằng số duy nhất cho 257
.
Tóm lại, trình biên dịch mã bytecode của Python không thể thực hiện tối ưu hóa lớn (như ngôn ngữ kiểu tĩnh), nhưng nó làm được nhiều hơn bạn nghĩ. Một trong những điều này là phân tích cách sử dụng các nghĩa đen và tránh sao chép chúng.
Lưu ý rằng điều này không liên quan đến bộ đệm ẩn, vì nó cũng hoạt động đối với các phao, không có bộ đệm:
>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True
Đối với các chữ phức tạp hơn, như bộ giá trị, nó "không hoạt động":
>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False
Nhưng các ký tự 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 PyInt_Object
được tạo ra, tôi đoán rằng điều này được thực hiện để tránh so sánh theo nghĩa đen. ví dụ: số 257
có thể được biểu thị bằng nhiều chữ:
>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257
Trình phân tích cú pháp có hai lựa chọn:
- Chuyển đổi các ký tự thành một số cơ sở chung trước khi tạo số nguyên và xem liệu các ký tự có tương đương hay khô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 không. Nếu có, chỉ giữ một giá trị duy nhất và gán nó cho tất cả các ký tự, nếu không, bạn đã có các số nguyên để gán.
Có lẽ trình phân tích cú pháp Python sử dụng cách tiếp cận thứ hai, tránh viết lại mã chuyển đổi và cũng dễ mở rộng hơn (ví dụ: nó cũng hoạt động với float).
Đọc Python/ast.c
tệp, hàm phân tích cú pháp tất cả các số parsenumber
, hàm này gọi PyOS_strtoul
để lấy giá trị số nguyên (đối với số nguyên) 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 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à suy đoán của tôi là đúng: trình phân tích cú pháp đầu tiên 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ã thực hiện kiểm tra này phải ở đâu đó trong Python/compile.c
hoặc Python/peephole.c
, vì đây là các tệp biến AST thành mã bytecode.
Đặc biệt, compiler_add_o
chức năng có vẻ là một trong những chức năng đó. Có bình luận 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 hàm / lambdas, v.v. compiler_add_o
Hàm lưu trữ các hằng số vào một dict
đối tượng và từ đó ngay lập tức các hằng số bằng nhau 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.