Hướng dẫn how do i use bytecode in python? - làm cách nào để sử dụng bytecode trong python?

Tìm hiểu về mã Python byte

Mã nguồn của ngôn ngữ lập trình có thể được thực thi bằng trình thông dịch hoặc trình biên dịch. Trong một ngôn ngữ được biên dịch, một trình biên dịch sẽ dịch mã nguồn trực tiếp thành mã máy nhị phân. Mã máy này dành riêng cho máy đích đó vì mỗi máy có thể có một hệ điều hành và phần cứng khác nhau. Sau khi biên dịch, máy đích sẽ trực tiếp chạy mã máy.

Trong một ngôn ngữ được giải thích, mã nguồn không được chạy trực tiếp bởi máy đích. Có một chương trình khác được gọi là trình thông dịch đọc và thực thi mã nguồn trực tiếp. Trình thông dịch, cụ thể cho máy đích, dịch từng câu lệnh của mã nguồn thành mã máy và chạy nó.

Python thường được gọi là ngôn ngữ được giải thích, tuy nhiên, nó kết hợp biên dịch và diễn giải. Khi chúng tôi thực thi mã nguồn (một tệp có tiện ích mở rộng

python -m compileall file_1.py ... file_n.py
17), trước tiên, Python biên dịch nó thành mã byte. Bytecode là một biểu diễn độc lập với nền tảng thấp của mã nguồn của bạn, tuy nhiên, đó không phải là mã máy nhị phân và không thể được chạy trực tiếp bởi máy đích. Trên thực tế, nó là một tập hợp các hướng dẫn cho một máy ảo được gọi là máy ảo Python (PVM).

Sau khi biên dịch, mã byte được gửi để thực hiện cho PVM. PVM là một trình thông dịch chạy mã byte và là một phần của hệ thống Python. Mã byte là độc lập với nền tảng, nhưng PVM là cụ thể cho máy đích. Việc triển khai mặc định của ngôn ngữ lập trình Python là CPython được viết bằng ngôn ngữ lập trình C. Cpython biên dịch mã nguồn Python vào mã byte và mã byte này sau đó được thực thi bởi máy ảo CPython.

Tạo tập tin bytecode

Trong Python, mã byte được lưu trữ trong tệp

python -m compileall file_1.py ... file_n.py
18. Trong Python 3, các tệp byte được lưu trữ trong một thư mục có tên
python -m compileall file_1.py ... file_n.py
19. Thư mục này được tự động tạo khi bạn cố gắng nhập một tệp khác mà bạn đã tạo:

import file_name

Tuy nhiên, nó sẽ không được tạo nếu chúng tôi không nhập tệp khác trong mã nguồn. Trong trường hợp đó, chúng ta vẫn có thể tạo ra nó theo cách thủ công. Để biên dịch các tệp riêng lẻ

python -m compileall file_1.py ... file_n.py
20 thành
python -m compileall file_1.py ... file_n.py
21 từ dòng lệnh, chúng ta có thể viết:

python -m compileall file_1.py ... file_n.py

Tất cả các tệp

python -m compileall file_1.py ... file_n.py
22 được tạo sẽ được lưu trữ trong
python -m compileall file_1.py ... file_n.py
23Folder. Nếu bạn không cung cấp tên tệp sau
python -m compileall file_1.py ... file_n.py
24, nó sẽ biên dịch tất cả các tệp mã nguồn Python trong thư mục hiện tại.

Chúng ta cũng có thể sử dụng hàm

python -m compileall file_1.py ... file_n.py
25 để biên dịch một chuỗi chứa mã nguồn Python. Cú pháp của chức năng này là:

python -m compileall file_1.py ... file_n.py
26

Chúng tôi chỉ tập trung vào ba đối số đầu tiên được yêu cầu (những đối số khác là tùy chọn).

python -m compileall file_1.py ... file_n.py
27 là mã nguồn để biên dịch có thể là một chuỗi, đối tượng byte hoặc đối tượng AST.
python -m compileall file_1.py ... file_n.py
28 là tên của tệp mà mã nguồn đến từ. Nếu mã nguồn không đến từ một tệp, bạn có thể viết bất cứ thứ gì bạn thích hoặc để lại một chuỗi trống.
python -m compileall file_1.py ... file_n.py
29 có thể là:

python -m compileall file_1.py ... file_n.py
30: Chấp nhận mã nguồn Python dưới mọi hình thức (bất kỳ số lượng câu lệnh hoặc khối). Nó biên dịch chúng thành một mã byte cuối cùng trả về
python -m compileall file_1.py ... file_n.py
31

python -m compileall file_1.py ... file_n.py
32: Chấp nhận một biểu thức duy nhất và biên dịch nó thành mã byte cuối cùng trả về giá trị của biểu thức đó

python -m compileall file_1.py ... file_n.py
33: Chỉ chấp nhận một câu lệnh duy nhất (hoặc nhiều câu lệnh được phân tách bằng
python -m compileall file_1.py ... file_n.py
34). Nếu câu lệnh cuối cùng là một biểu thức, thì mã byte kết quả sẽ in, giá trị của biểu thức đó thành đầu ra tiêu chuẩn.

Ví dụ: để biên dịch một số câu lệnh Python chúng ta có thể viết:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")

hoặc viết tương đương:

compile("a=5 \na+=1 \nprint(a)", "", "exec")

Để đánh giá một biểu thức chúng ta có thể viết:

compile("a+7", "", "eval")

Chế độ này gây ra lỗi nếu bạn không có biểu thức:

# This does not work:
compile("a=a+1", "", "eval")

Ở đây

python -m compileall file_1.py ... file_n.py
36 không phải là một biểu thức và không trả lại bất cứ điều gì, vì vậy chúng tôi không thể sử dụng chế độ
python -m compileall file_1.py ... file_n.py
37. Tuy nhiên, chúng ta có thể sử dụng chế độ
python -m compileall file_1.py ... file_n.py
38 để biên dịch nó:

compile("a=a+1", "", "single")

Nhưng những gì được trả lại bởi

python -m compileall file_1.py ... file_n.py
39? Khi bạn chạy chức năng
python -m compileall file_1.py ... file_n.py
39, Python trả về:

 at 0x000001A1DED95540, file "", line 1>

Vì vậy, những gì hàm

python -m compileall file_1.py ... file_n.py
39 đang trả về là một đối tượng mã (địa chỉ sau
python -m compileall file_1.py ... file_n.py
42 có thể khác nhau trên máy của bạn).

Đối tượng mã

Hàm

python -m compileall file_1.py ... file_n.py
25 trả về một đối tượng mã Python. Tất cả mọi thứ trong Python là một đối tượng. Ví dụ: chúng tôi xác định một biến số nguyên, giá trị của nó được lưu trữ trong đối tượng
python -m compileall file_1.py ... file_n.py
44 và bạn có thể dễ dàng kiểm tra loại của nó bằng hàm
python -m compileall file_1.py ... file_n.py
45:

a = 5
type(a) # Output is: int

Theo một cách tương tự, mã byte được tạo bởi hàm biên dịch được lưu trữ trong đối tượng

python -m compileall file_1.py ... file_n.py
46.

c = compile("a=a+1", "", "single")
type(c) # Output is: code

Đối tượng mã không chỉ chứa mã byte mà còn một số thông tin khác cần thiết để cpython chạy mã byte (chúng sẽ được thảo luận sau). Một đối tượng mã có thể được thực thi hoặc đánh giá bằng cách chuyển nó cho hàm

python -m compileall file_1.py ... file_n.py
47 hoặc
python -m compileall file_1.py ... file_n.py
48. Vì vậy, chúng tôi có thể viết:

python -m compileall file_1.py ... file_n.py
0

Khi bạn xác định một hàm trong Python, nó sẽ tạo một đối tượng mã cho nó và bạn có thể truy cập nó bằng thuộc tính

python -m compileall file_1.py ... file_n.py
49. Ví dụ: chúng ta có thể viết:

python -m compileall file_1.py ... file_n.py
1

Và đầu ra sẽ là:

python -m compileall file_1.py ... file_n.py
2

Giống như bất kỳ đối tượng nào khác, đối tượng mã có một số thuộc tính và để nhận mã byte được lưu trữ trong một đối tượng mã, bạn có thể sử dụng thuộc tính

python -m compileall file_1.py ... file_n.py
50 của nó:

python -m compileall file_1.py ... file_n.py
3

Đầu ra là:

python -m compileall file_1.py ... file_n.py
4

Kết quả là một byte theo nghĩa đen được đặt trước với

python -m compileall file_1.py ... file_n.py
51, nó là một chuỗi byte bất biến và có một loại
python -m compileall file_1.py ... file_n.py
52. Mỗi byte có thể có giá trị thập phân từ 0 đến 255. Vì vậy, một byte theo nghĩa đen là một chuỗi số nguyên bất động trong khoảng từ 0 đến 255. Mỗi byte có thể được hiển thị bởi một ký tự ASCII có mã ký tự giống như giá trị byte hoặc nó có thể Thể hiện bởi một
python -m compileall file_1.py ... file_n.py
53 hàng đầu theo sau là hai ký tự. Escape
python -m compileall file_1.py ... file_n.py
53 hàng đầu có nghĩa là hai ký tự tiếp theo được hiểu là các chữ số hex cho mã ký tự. Ví dụ:

python -m compileall file_1.py ... file_n.py
5

gives:

python -m compileall file_1.py ... file_n.py
6

Vì phần tử đầu tiên có giá trị thập phân là 101 và có thể được hiển thị với ký tự

python -m compileall file_1.py ... file_n.py
55 có mã ký tự ASCII là 101. hoặc::

python -m compileall file_1.py ... file_n.py
7

gives:

python -m compileall file_1.py ... file_n.py
8

Vì phần tử thứ 4 có giá trị thập phân là 131. Giá trị thập lục phân là 131 là 83. Vì vậy, byte này có thể được hiển thị với một ký tự có mã ký tự là

python -m compileall file_1.py ... file_n.py
56.

Các chuỗi byte này có thể được giải thích bằng Cpython, nhưng chúng không thân thiện với con người. Vì vậy, chúng ta cần hiểu làm thế nào các byte này được ánh xạ tới các hướng dẫn thực tế sẽ được thực thi bởi CPython. Trong phần tiếp theo, chúng tôi sẽ tháo rời mã byte thành một số hướng dẫn thân thiện với con người để xem mã byte được thực thi bởi CPython như thế nào.

Chi tiết mã byte

Trước khi đi sâu vào chi tiết, điều quan trọng cần lưu ý là việc triển khai chi tiết mã byte thường thay đổi giữa các phiên bản của Python. Vì vậy, những gì bạn thấy trong bài viết này có thể không hợp lệ cho tất cả các phiên bản của Python. Trên thực tế, nó bao gồm các thay đổi xảy ra trong phiên bản 3.6 và một số chi tiết có thể không hợp lệ cho các phiên bản cũ hơn. Mã trong bài viết này đã được thử nghiệm với Python 3.7.

Mã byte có thể được coi là một loạt các hướng dẫn hoặc một chương trình cấp thấp cho trình thông dịch Python. Sau phiên bản 3.6, Python sử dụng 2 byte cho mỗi lệnh. Một byte là cho mã của hướng dẫn đó được gọi là opcode và một byte được dành riêng cho itargumentwhich được gọi là oparg. Mỗi opcode có một tên thân thiện với con người được gọi là opname. Các hướng dẫn bytecode có định dạng chung như thế này:

python -m compileall file_1.py ... file_n.py
9

Chúng tôi đã có mã hóa trong mã byte của chúng tôi và chúng tôi chỉ cần ánh xạ chúng đến tên opname tương ứng của chúng. Có một mô -đun gọi là

python -m compileall file_1.py ... file_n.py
57 có thể giúp với điều đó. Trong mô -đun này, có một danh sách gọi là
python -m compileall file_1.py ... file_n.py
58 lưu trữ tất cả các tên opnames. Phần tử thứ i của danh sách này cung cấp tên opname cho một lệnh có mã opcode bằng i.

Một số hướng dẫn không cần một đối số, vì vậy họ bỏ qua byte sau opcode. Các opcodes có giá trị dưới một số nhất định bỏ qua đối số của họ. Giá trị này được lưu trữ trong

python -m compileall file_1.py ... file_n.py
59 và hiện bằng 90. Vì vậy, các opcodes> = ________ 159 có một đối số và các opcodes <
python -m compileall file_1.py ... file_n.py
59 bỏ qua nó.

Ví dụ, giả sử rằng chúng ta có một mã byte ngắn

python -m compileall file_1.py ... file_n.py
62 và chúng ta muốn tháo rời nó. Mã byte này đại diện cho một chuỗi bốn byte. Chúng ta có thể dễ dàng hiển thị giá trị thập phân của chúng:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
0

Đầu ra sẽ là:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
1

Hai byte đầu tiên của mã byte là

python -m compileall file_1.py ... file_n.py
63. Byte đầu tiên là opcode. Để có được tên opname của nó, chúng ta có thể viết (
python -m compileall file_1.py ... file_n.py
57 nên được nhập trước):

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
2

và kết quả là

python -m compileall file_1.py ... file_n.py
65. Vì opcode lớn hơn
python -m compileall file_1.py ... file_n.py
59, nên nó có Oparg là byte thứ hai
python -m compileall file_1.py ... file_n.py
67. Vì vậy,
python -m compileall file_1.py ... file_n.py
63 dịch thành:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
3

Hai byte cuối cùng trong mã byte là

python -m compileall file_1.py ... file_n.py
69. Một lần nữa chúng tôi viết
python -m compileall file_1.py ... file_n.py
70 và kết quả là
python -m compileall file_1.py ... file_n.py
71. 83 thấp hơn 90 (
python -m compileall file_1.py ... file_n.py
59), do đó, opcode này bỏ qua oparg và
python -m compileall file_1.py ... file_n.py
69 được tháo rời vào:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
4

Ngoài ra, một số hướng dẫn có thể có một đối số quá lớn để phù hợp với một byte mặc định. Có một opcode đặc biệt

python -m compileall file_1.py ... file_n.py
74 để xử lý các hướng dẫn này. Tên opname của nó là
python -m compileall file_1.py ... file_n.py
75, và nó cũng được lưu trữ trong
python -m compileall file_1.py ... file_n.py
76. Opcode này có tiền tố bất kỳ opcode nào có đối số lớn hơn một byte. Ví dụ: giả sử rằng chúng ta có opcode 131 (tên opname của nó là
python -m compileall file_1.py ... file_n.py
77) và Oparg của nó cần phải là 260. Vì vậy, nó phải là:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
5

Tuy nhiên, số lượng tối đa mà một byte có thể lưu trữ là 255 và 260 không phù hợp với byte. Vì vậy, opcode này được tiền tố với

python -m compileall file_1.py ... file_n.py
75:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
6

Khi trình thông dịch thực thi

python -m compileall file_1.py ... file_n.py
75, oparg của nó (là 1) được dịch chuyển trái bởi tám bit và được lưu trữ trong một biến tạm thời. Hãy để Lừa gọi nó là
python -m compileall file_1.py ... file_n.py
80 (đừng nhầm lẫn với opname
python -m compileall file_1.py ... file_n.py
75):

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
7

Vì vậy, giá trị nhị phân

python -m compileall file_1.py ... file_n.py
82 (giá trị nhị phân của 1) được chuyển đổi thành
python -m compileall file_1.py ... file_n.py
83. Điều này giống như nhân 1 với 256 trong hệ thống thập phân và
python -m compileall file_1.py ... file_n.py
84 sẽ bằng 256. Bây giờ chúng ta có hai byte trong
python -m compileall file_1.py ... file_n.py
84. Khi trình thông dịch đạt được hướng dẫn tiếp theo, giá trị hai byte này được thêm vào oparg của nó (là 4 ở đây) bằng cách sử dụng bitwise
python -m compileall file_1.py ... file_n.py
86.

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
8

Điều này giống như thêm giá trị của OPARG vào

python -m compileall file_1.py ... file_n.py
84. Vì vậy, bây giờ chúng tôi có:

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
9

và giá trị này sẽ được sử dụng làm oparg thực tế của

python -m compileall file_1.py ... file_n.py
77. Vì vậy, trên thực tế,

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
6

được giải thích là:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
1

Đối với mỗi opcode, nhiều nhất là ba tiền tố

python -m compileall file_1.py ... file_n.py
75 được cho phép, tạo thành một đối số từ hai byte đến bốn byte.

Bây giờ chúng ta có thể tập trung vào chính Oparg. Nó có nghĩa là gì? Trên thực tế, ý nghĩa của mỗi oparg phụ thuộc vào opcode của nó. Như đã đề cập trước đây, đối tượng mã lưu trữ một số thông tin khác ngoài mã byte. Thông tin này có thể được truy cập bằng cách sử dụng các thuộc tính khác nhau của đối tượng mã và chúng tôi cần một số thuộc tính này để giải mã ý nghĩa của mỗi oparg. Các thuộc tính này là:

python -m compileall file_1.py ... file_n.py
90,
python -m compileall file_1.py ... file_n.py
91,
python -m compileall file_1.py ... file_n.py
92,
python -m compileall file_1.py ... file_n.py
93 và
python -m compileall file_1.py ... file_n.py
94.

Thuộc tính đối tượng mã

Tôi sẽ giải thích ý nghĩa của các thuộc tính này bằng một ví dụ. Giả sử bạn có đối tượng mã của mã nguồn này:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
2

Bây giờ chúng ta có thể kiểm tra những gì được lưu trữ trong mỗi thuộc tính này:

1 -________ 190: Một tuple chứa các chữ được sử dụng bởi mã byte. Ở đây

python -m compileall file_1.py ... file_n.py
96 Trả về:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
3

Vì vậy, các chữ

python -m compileall file_1.py ... file_n.py
97 và
python -m compileall file_1.py ... file_n.py
98 và tên của hàm
python -m compileall file_1.py ... file_n.py
99 đều được lưu trữ trong bộ tuple này. Ngoài ra, phần thân của hàm
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 được lưu trữ trong một đối tượng mã riêng biệt và được đối xử như một chữ theo nghĩa đen cũng được lưu trữ trong bộ tu này. Hãy nhớ rằng chế độ
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
01 trong
python -m compileall file_1.py ... file_n.py
25 tạo mã byte cuối cùng trả về
python -m compileall file_1.py ... file_n.py
31. Giá trị
python -m compileall file_1.py ... file_n.py
31 này cũng được lưu trữ dưới dạng theo nghĩa đen. Trên thực tế, nếu bạn biên dịch một biểu thức trong chế độ
python -m compileall file_1.py ... file_n.py
37 như thế này:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
4

python -m compileall file_1.py ... file_n.py
31 won đã được đưa vào bộ
python -m compileall file_1.py ... file_n.py
90 nữa. Lý do là biểu thức này trả về giá trị cuối cùng của nó không phải
python -m compileall file_1.py ... file_n.py
31.

Nếu bạn cố gắng lấy

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
09 cho mã đối tượng của một hàm như:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
5

Kết quả sẽ là

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
10. Trên thực tế, giá trị trả về mặc định cho một hàm là
python -m compileall file_1.py ... file_n.py
31 và nó luôn được thêm vào dưới dạng theo nghĩa đen. Như tôi đã giải thích sau, vì lợi ích của hiệu quả, Python không kiểm tra xem bạn có luôn luôn đạt được câu lệnh
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
12 hay không, vì vậy
python -m compileall file_1.py ... file_n.py
31 luôn được thêm vào làm giá trị trả về mặc định.

2 -________ 191: Một tuple chứa các tên được sử dụng bởi mã byte có thể là các biến, hàm và các lớp toàn cầu hoặc các thuộc tính được tải từ các đối tượng. Ví dụ, đối với mã đối tượng trong Liệt kê 1,

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
15 đưa ra:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
6

3 -________ 192: Một tuple chứa các tên cục bộ được sử dụng bởi mã byte (đối số đầu tiên, sau đó là các biến cục bộ). Nếu chúng tôi thử nó cho mã đối tượng của Danh sách 1, nó sẽ cho một bộ xử lý trống. Lý do là các tên cục bộ được xác định bên trong các chức năng và hàm bên trong Liệt kê 1 được lưu trữ dưới dạng đối tượng mã riêng biệt, do đó các biến cục bộ của nó sẽ không được đưa vào trong bộ thuật này. Để truy cập các biến cục bộ của một hàm, chúng ta nên sử dụng thuộc tính này cho đối tượng mã của hàm đó. Vì vậy, trước tiên chúng tôi viết mã nguồn này:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
7

Bây giờ

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
17 cung cấp cho đối tượng mã của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 và
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
19 đưa ra:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
8

Tại sao

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
20 không được bao gồm? Lý do là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
20 không phải là biến cục bộ là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00. Đây là một biến không thuộc địa do nó được truy cập bởi việc đóng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
23 bên trong
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00. Trên thực tế,
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
25 cũng là một biến không thuộc địa điểm, nhưng vì đó là đối số chức năng, nó luôn được bao gồm trong bộ tu này. Để tìm hiểu thêm về việc đóng cửa và các biến phi địa phương, bạn có thể tham khảo bài viết này.

4 -________ 193: Một tuple chứa tên của các biến không thuộc địa. Đây là các biến cục bộ của một hàm được truy cập bởi các hàm bên trong của nó. Vì vậy,

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
27 đưa ra:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
9

5 -________ 194: Một tuple chứa tên của các biến miễn phí. Các biến miễn phí là các biến cục bộ của hàm bên ngoài được truy cập bởi hàm bên trong của nó. Vì vậy, thuộc tính này nên được sử dụng với đối tượng mã của việc đóng

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
29. Bây giờ
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
30 cho kết quả tương tự:A tuple containing the names of free variables. Free variables are the local variables of an outer function which are accessed by its inner function. So this attribute should be used with the code object of the closure
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
29. Now
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
30 gives the same result:

compile("a=5 \na+=1 \nprint(a)", "", "exec")
9

Bây giờ chúng ta đã quen thuộc với các thuộc tính này, chúng ta có thể quay lại Opargs. Ý nghĩa của mỗi oparg phụ thuộc vào opcode của nó. Chúng tôi có các loại opcode khác nhau và đối với mỗi loại, oparg có một ý nghĩa khác nhau. Trong mô -đun

python -m compileall file_1.py ... file_n.py
57, có một số danh sách cung cấp mã hóa cho từng loại:

1 -________ 232: Danh sách này bằng [100]. Vì vậy, chỉ có opcode 100 (opname của nó là load_const) nằm trong danh mục

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
33. Oparg của opcode này cung cấp chỉ số của một phần tử trong bộ
python -m compileall file_1.py ... file_n.py
90. Ví dụ: trong mã byte của Danh sách 1, nếu chúng ta có:

compile("a+7", "", "eval")
1

sau đó OPARG là yếu tố của

python -m compileall file_1.py ... file_n.py
90 có chỉ số là 1. vì vậy chúng ta nên thay thế
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36 bằng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
37 bằng
python -m compileall file_1.py ... file_n.py
98. Vì vậy, hướng dẫn sẽ được giải thích là:

compile("a+7", "", "eval")
2

Tương tự, có một số danh sách khác trong mô -đun

python -m compileall file_1.py ... file_n.py
57 xác định các danh mục khác cho các opcodes:

2 -________ 240: Oparg cho các opcode trong danh sách này, là chỉ số của một phần tử trong

python -m compileall file_1.py ... file_n.py
91

3 -________ 242: Oparg cho các opcode trong danh sách này, là chỉ số của một yếu tố trong

python -m compileall file_1.py ... file_n.py
92

4 -________ 244: Oparg cho các opcode trong danh sách này, là chỉ số của một phần tử trong

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
45

5 -________ 246: Oparg cho opcode trong danh sách này, là chỉ số của một phần tử của tuple

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
47. Tuple này chứa các nhà khai thác so sánh và tư cách thành viên như
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
48 hoặc
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
49

6 -________ 250: OPARG cho các opcode trong danh sách này, nên được thay thế bằng

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
51 trong đó
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
52 là chỉ số của byte trong chuỗi bytecode đại diện cho opcode.

Đối tượng mã có một thuộc tính quan trọng hơn cần được thảo luận ở đây. Nó được gọi là

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 lưu trữ thông tin số dòng của mã byte. Đây là một mảng các byte được ký kết được lưu trữ trong một byte theo nghĩa đen và được sử dụng để ánh xạ các độ lệch mã byte cho các số dòng mã nguồn. Hãy để tôi giải thích nó bằng một ví dụ. Giả sử rằng mã nguồn của bạn chỉ có ba dòng và nó đã được biên dịch thành mã byte có 24 byte:

compile("a+7", "", "eval")
3

Bây giờ chúng tôi có một ánh xạ từ mã ByteCode đến các số dòng như bảng này:

ByteCode Offset luôn bắt đầu ở 0. Đối tượng mã có một thuộc tính có tên

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
54 cung cấp số dòng cho số bù 0. Đối với ví dụ này
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
54 bằng 1. Thay vì lưu trữ các số bù và dòng theo nghĩa đen, Python chỉ lưu trữ các mức tăng từ hàng này sang hàng tiếp theo (không bao gồm hàng đầu tiên). Vì vậy, bảng trước đó biến thành:

Hai cột gia tăng này được nén lại với nhau theo một chuỗi như thế này:

compile("a+7", "", "eval")
4

Mỗi số được lưu trữ trong một byte và toàn bộ chuỗi được lưu trữ dưới dạng byte theo nghĩa đen trong

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 của đối tượng mã. Vì vậy, nếu bạn kiểm tra giá trị của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 bạn nhận được:

compile("a+7", "", "eval")
5

đó là các byte theo nghĩa đen cho chuỗi trước. Vì vậy, bằng cách có các thuộc tính

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 và
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
54, bạn có thể truy xuất ánh xạ từ độ lệch mã byte đến các số dòng mã nguồn.
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 là một chuỗi các byte có chữ ký. Vì vậy, mỗi byte được ký trong đó có thể lấy giá trị từ -128 đến 127 (các giá trị này vẫn được lưu trữ trong một byte có 0 đến 255. nhưng giá trị từ 128 đến 255 được coi là số âm). Một mức tăng âm có nghĩa là số dòng đang giảm (tính năng này được sử dụng trong các trình tối ưu hóa). Nhưng điều gì xảy ra nếu mức tăng dòng lớn hơn 127? Trong trường hợp đó, mức tăng dòng sẽ được chia thành 127 và một số byte bổ sung và các byte bổ sung đó sẽ được lưu trữ với mức tăng bù 0 (nếu nó nhỏ hơn -128, nó sẽ được chia thành -128 và một số byte bổ sung với tăng áp gia tăng không). Ví dụ: giả sử rằng phần bù bytecode so với số dòng giống như sau:

Sau đó, mức tăng bù so với mức tăng số dòng phải là:

139 bằng 127 + 12. Vì vậy, hàng trước nên được viết là:

và nên được lưu trữ dưới dạng

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
61. Vì vậy, giá trị của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 sẽ là:
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
63.

Tháo rời mã byte

Bây giờ chúng tôi đã quen thuộc với cấu trúc byte, chúng tôi có thể viết một chương trình Disassembler đơn giản. Trước tiên, chúng tôi viết một hàm trình tạo để giải nén từng lệnh và mang lại phần bù, opcode và oparg:

Hàm này đọc cặp byte tiếp theo từ mã byte. Byte đầu tiên là opcode. Bằng cách so sánh opcode này với

python -m compileall file_1.py ... file_n.py
59, chức năng quyết định xem nó có nên lấy byte thứ hai làm oparg hay bỏ qua nó không. Giá trị của
python -m compileall file_1.py ... file_n.py
80 sẽ được thêm vào Oparg bằng cách sử dụng bitwise hoặc (
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
66). Ban đầu, nó bằng không và không có tác dụng đối với Oparg. Nếu opcode bằng
python -m compileall file_1.py ... file_n.py
76, OPARG của nó sẽ được dịch chuyển trái bởi tám bit và được lưu trữ trong một biến tạm thời gọi là
python -m compileall file_1.py ... file_n.py
80.

Trong lần lặp tiếp theo, biến tạm thời này sẽ được thêm vào OPARG tiếp theo và thêm một byte vào nó. Quá trình này tiếp tục nếu opcode tiếp theo là

python -m compileall file_1.py ... file_n.py
76 một lần nữa và mỗi lần thêm một byte vào
python -m compileall file_1.py ... file_n.py
80. Cuối cùng, khi nó đạt đến một opcode khác,
python -m compileall file_1.py ... file_n.py
80 sẽ được thêm vào oparg của nó và đặt trở lại 0.

The

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
72 function returns a dictionary that contains the source code line number for each bytecode offset.

It first divided the

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
53 bytes literal into two sequences. One is the offset increments and the other is the line number increments. The line number for offset
python -m compileall file_1.py ... file_n.py
67 is in
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
54. The increments are added to these two numbers to get the bytecode offset and its corresponding line number. If the line number increment is equal or bigger than 128 (0x80), it will be considered a decrement.

The

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
76 function returns the human-friendly meaning of each oparg. It first checks to which category the opcode belongs and then figures out what the oparg is referring to.

The

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
77 function finds all the offsets in the bytecode which are jump targets and returns a list of these offsets. The jump targets will be discussed in the next section.

Now we can use all these functions to disassemble the bytecode. The

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
78 function takes a code object and disassembles it:

It will first unpack the offset, opcode and oparg for each pair of bytes in the bytecode of the code object. Then it finds the corresponding source code line numbers, and checks if the offset is a jump target. Finally, it finds the opname and the meaning of the oparg and prints all the information. As mentioned before each function definition is stored in a separate code object. So at the end the function calls itself recursively to disassemble all the function definitions in the bytecode. Here is an example of using this function. Initially, we have this source code:

compile("a+7", "", "eval")
6

We first store it in a string and compile it to get the object code. Then we use the

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
79 function to disassemble its bytecode:

compile("a+7", "", "eval")
7

The output is:

compile("a+7", "", "eval")
8

So 4 lines of source code are converted into 38 bytes of bytecode or 19 lines of bytecode. In the next section, I will explain the meaning of these instructions and how they will be interpreted by CPython.

The module

python -m compileall file_1.py ... file_n.py
57 has a function named
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
81 which can disassemble the code object similarly. In fact, the
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
82 function in this article is a simplified version of
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
83 function. So instead of writing,
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
84 we could write
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
85 to get a similar output.

Disassembling a pyc file

As mentioned before, when the source code is compiled, the bytecode is stored in a

python -m compileall file_1.py ... file_n.py
22 file. This bytecode can be disassembled in a similar way. However, it is important to mention that the
python -m compileall file_1.py ... file_n.py
22 file contains some metadata plus the code object in marshal format. The marshal format is used for Python’s internal object serialization. The size of the metadata depends on the Python version, and for version 3.7 it is 16 bytes. So when you read the
python -m compileall file_1.py ... file_n.py
22 file, first you should read the metadata, and then load the code object using the
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
89 module. For example, to disassemble a
python -m compileall file_1.py ... file_n.py
22 file named
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
91 in the
python -m compileall file_1.py ... file_n.py
19 folder we can write:

Bytecode operations

So far we learned how to disassemble the bytecode instructions. We can now focus on the meaning of these instructions and how they are executed by CPython. CPython which is the default implementation of Python uses a stack-based virtual machine. So first we should get familiar with the stack.

Stack and heap

Stack is a data structure with a LIFO (Last In First Out) order. It has two principal operations:

  • push: adds an element to the stack
  • pop: removes the most recently added element

So the last element added or pushed to the stack is the first element to be removed or popped. The advantage of using stack to store data is that memory is managed for you. Reading from and writing to stack is very fast, however, the size of stack is limited.

Data in Python is represented as objects stored on a private heap. Accessing the data on heap is a bit slower compared to stack, however, the size of heap is only limited by the size of virtual memory. The elements of heap have no dependencies with each other and can be accessed randomly at any time. Everything in Python is an object and objects are always stored on the heap. It’s only the reference (or the pointer) to the object that is stored in the stack.

CPython uses the call stack for running a Python program. When a function is called in Python, a new frameis pushed onto the call stack, and every time a function call returns, its frame is popped off. The module in which the program runs has the bottom-most frame which is called the global frame or the module frame.

Each frame has an evaluation stack where the execution of a Python function occurs. The function arguments and its local variables are pushed into this evaluation stack. CPython uses the evaluation stack to store the parameters required for any operations and also the result of those operations. Before starting that operation, all the required parameters are pushed onto the evaluation stack. Then the operation is started and it pops its parameters. When the operation is finished, it pushes the result back onto the evaluation stack.

Tất cả các đối tượng được lưu trữ trên đống và ngăn xếp đánh giá trong các khung liên quan đến các tham chiếu đến chúng. Vì vậy, các tham chiếu đến các đối tượng này có thể được đẩy vào ngăn xếp đánh giá tạm thời được sử dụng cho các hoạt động sau này. Hầu hết các hướng dẫn mã byte Python, thao tác ngăn xếp đánh giá trong khung hiện tại. Trong bài viết này bất cứ khi nào chúng ta nói về ngăn xếp, điều đó có nghĩa là ngăn xếp đánh giá trong khung hiện tại hoặc ngăn xếp đánh giá trong khung toàn cầu nếu chúng ta không ở trong phạm vi của bất kỳ chức năng nào.

Hãy để tôi bắt đầu với một ví dụ đơn giản và tháo rời mã byte của mã nguồn sau:

compile("a+7", "", "eval")
9

Để làm điều đó, chúng ta có thể viết:

# This does not work:
compile("a=a+1", "", "eval")
0

Và chúng tôi nhận được:

# This does not work:
compile("a=a+1", "", "eval")
1

Ngoài ra, chúng ta có thể kiểm tra một số thuộc tính khác của đối tượng mã:

# This does not work:
compile("a=a+1", "", "eval")
2

Ở đây mã đang chạy trong mô -đun, vì vậy chúng tôi đang ở trong khung toàn cầu. Hướng dẫn đầu tiên là

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
93. Hướng dẫn

# This does not work:
compile("a=a+1", "", "eval")
3

Đẩy giá trị của

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
94 lên ngăn xếp. Vì vậy, chúng tôi đang đẩy ________ 295 (bằng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36) lên ngăn xếp.

Điều quan trọng cần lưu ý là Stack hoạt động với các tham chiếu đến các đối tượng. Vì vậy, bất cứ khi nào chúng ta nói rằng một lệnh đẩy một đối tượng hoặc giá trị của một đối tượng lên ngăn xếp, điều đó có nghĩa là một tham chiếu (hoặc con trỏ) đến đối tượng đó đang được đẩy. Điều tương tự cũng xảy ra khi một đối tượng hoặc giá trị của nó được bật ra khỏi ngăn xếp. Một lần nữa tài liệu tham khảo của nó được bật lên. Trình thông dịch biết cách truy xuất hoặc lưu trữ dữ liệu của đối tượng bằng các tài liệu tham khảo này.

Hướng dẫn

# This does not work:
compile("a=a+1", "", "eval")
4

Bật phần trên cùng của ngăn xếp và lưu trữ nó vào một đối tượng có tài liệu tham khảo được lưu trữ trong

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
97 của đối tượng mã. Vì vậy,
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
98 bật phần tử trên đầu ngăn xếp (là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36) và lưu trữ nó trong một đối tượng. Tham chiếu đến đối tượng này là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
00 là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
01. Hai hướng dẫn này là mã byte tương đương với
compile("a=5 \na+=1 \nprint(a)", "", "exec")
02 trong mã nguồn.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
03 được chuyển đổi tương tự và bây giờ trình thông dịch đã tạo các đối tượng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
01 và
compile("a=5 \na+=1 \nprint(a)", "", "exec")
05. Dòng cuối cùng của mã nguồn là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
06. Hướng dẫn

# This does not work:
compile("a=a+1", "", "eval")
5

Bật hai yếu tố hàng đầu của ngăn xếp (

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36 và
compile("a=5 \na+=1 \nprint(a)", "", "exec")
08), thêm chúng lại với nhau và đẩy kết quả (
compile("a=5 \na+=1 \nprint(a)", "", "exec")
09) vào ngăn xếp. Vì vậy, bây giờ
compile("a=5 \na+=1 \nprint(a)", "", "exec")
09 là trên đỉnh của ngăn xếp. Sau đó
compile("a=5 \na+=1 \nprint(a)", "", "exec")
11 bật đỉnh của ngăn xếp vào đối tượng cục bộ (được giới thiệu bởi)
compile("a=5 \na+=1 \nprint(a)", "", "exec")
12. Bây giờ hãy nhớ rằng
python -m compileall file_1.py ... file_n.py
39 trong
compile("a=5 \na+=1 \nprint(a)", "", "exec")
14Mode biên dịch mã nguồn thành mã byte cuối cùng trả về
python -m compileall file_1.py ... file_n.py
31. Hướng dẫn
compile("a=5 \na+=1 \nprint(a)", "", "exec")
16 đẩy
compile("a=5 \na+=1 \nprint(a)", "", "exec")
17 lên ngăn xếp và hướng dẫn

# This does not work:
compile("a=a+1", "", "eval")
6

Trả về với phần trên cùng của ngăn xếp cho người gọi của chức năng. Tất nhiên, ở đây chúng tôi đang ở trong phạm vi mô -đun và không có chức năng người gọi, vì vậy

python -m compileall file_1.py ... file_n.py
31 là kết quả cuối cùng vẫn còn trên đỉnh của ngăn xếp toàn cầu. Hình 1 cho thấy tất cả các hoạt động bytecode với độ lệch 0 đến 14 (một lần nữa cần lưu ý rằng các tham chiếu đến các đối tượng được đẩy lên ngăn xếp, không phải các đối tượng hoặc giá trị của chúng. Hình không hiển thị rõ ràng).

Chức năng, biến toàn cầu và địa phương

Bây giờ, hãy để xem những gì xảy ra nếu chúng ta cũng có một chức năng. Chúng tôi sẽ tháo rời mã byte của mã nguồn có chức năng:

# This does not work:
compile("a=a+1", "", "eval")
7

Đầu ra là:

# This does not work:
compile("a=a+1", "", "eval")
8

Ngoài ra, chúng ta có thể kiểm tra một số thuộc tính khác của đối tượng mã:

# This does not work:
compile("a=a+1", "", "eval")
9

Ở đây mã đang chạy trong mô -đun, vì vậy chúng tôi đang ở trong khung toàn cầu. Hướng dẫn đầu tiên là

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
93. Hướng dẫn

Đẩy giá trị của

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
94 lên ngăn xếp. Vì vậy, chúng tôi đang đẩy ________ 295 (bằng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36) lên ngăn xếp.

compile("a=a+1", "", "single")
0

Điều quan trọng cần lưu ý là Stack hoạt động với các tham chiếu đến các đối tượng. Vì vậy, bất cứ khi nào chúng ta nói rằng một lệnh đẩy một đối tượng hoặc giá trị của một đối tượng lên ngăn xếp, điều đó có nghĩa là một tham chiếu (hoặc con trỏ) đến đối tượng đó đang được đẩy. Điều tương tự cũng xảy ra khi một đối tượng hoặc giá trị của nó được bật ra khỏi ngăn xếp. Một lần nữa tài liệu tham khảo của nó được bật lên. Trình thông dịch biết cách truy xuất hoặc lưu trữ dữ liệu của đối tượng bằng các tài liệu tham khảo này.

Hướng dẫn

compile("a=a+1", "", "single")
1

được sử dụng để tạo chức năng. Nó cần một số thông số nên được đẩy lên ngăn xếp. Tên của chức năng phải nằm trên đầu ngăn xếp và đối tượng mã chức năng phải ở bên dưới nó. Trong ví dụ này, OPARG của nó bằng không, nhưng nó có thể có các giá trị khác. Ví dụ: nếu định nghĩa hàm có đối số từ khóa như:

compile("a=a+1", "", "single")
2

Sau đó, mã byte được tháo rời cho dòng 2 sẽ là:

compile("a=a+1", "", "single")
3

Một oparg là

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36 cho
compile("a=5 \na+=1 \nprint(a)", "", "exec")
36 chỉ ra rằng hàm có một số đối số từ khóa và một tuple chứa các giá trị mặc định phải được đẩy lên ngăn xếp trước đối tượng mã hàm (ở đây là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
37). Sau khi tạo chức năng,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
36 đẩy đối tượng chức năng mới lên ngăn xếp. Sau đó, tại Offset 14,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
11 bật đối tượng chức năng và lưu trữ nó dưới dạng đối tượng hàm được tham chiếu bởi
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00.

Bây giờ, hãy để Lừa nhìn vào bên trong đối tượng mã của

compile("a=5 \na+=1 \nprint(a)", "", "exec")
41 bắt đầu ở dòng 5. Câu lệnh
compile("a=5 \na+=1 \nprint(a)", "", "exec")
42 không chuyển đổi thành một lệnh riêng biệt trong mã byte. Nó chỉ hướng dẫn trình biên dịch rằng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
01 nên được coi là một biến toàn cầu. Vì vậy,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
44 sẽ được sử dụng để thay đổi giá trị của nó. Hướng dẫn

compile("a=a+1", "", "single")
4

Đẩy một tham chiếu đến đối tượng được đề cập bởi

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
97 lên ngăn xếp. Sau đó, nó được lưu trữ trong
compile("a=5 \na+=1 \nprint(a)", "", "exec")
05 bằng cách sử dụng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
44. Hướng dẫn

compile("a=a+1", "", "single")
5

Đẩy một tham chiếu đến đối tượng có tham chiếu là

compile("a=5 \na+=1 \nprint(a)", "", "exec")
48 lên ngăn xếp. Trong đối tượng mã của hàm
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00, thuộc tính
python -m compileall file_1.py ... file_n.py
92 chứa:

compile("a=a+1", "", "single")
6

Vì vậy,

compile("a=5 \na+=1 \nprint(a)", "", "exec")
51 đẩy
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
25 lên ngăn xếp. Sau đó
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36 được đẩy lên ngăn xếp.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
54 Pops
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
25 và
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36, thêm chúng lại với nhau và đẩy kết quả lên ngăn xếp. Hướng dẫn

compile("a=a+1", "", "single")
7

Bật đỉnh của ngăn xếp và lưu trữ nó vào một đối tượng có tài liệu tham khảo được lưu trữ trong

compile("a=5 \na+=1 \nprint(a)", "", "exec")
48. Vì vậy,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
58 bật kết quả và lưu trữ nó trong một đối tượng có tham chiếu là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
59.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60 và
compile("a=5 \na+=1 \nprint(a)", "", "exec")
61 được sử dụng với các biến cục bộ của các hàm. Vì vậy, chúng không được sử dụng ở phạm vi mô -đun. Mặt khác,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
62 và
compile("a=5 \na+=1 \nprint(a)", "", "exec")
63 được sử dụng cho các biến toàn cầu được truy cập bên trong các chức năng. Cuối cùng,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
64 sẽ đẩy giá trị của
compile("a=5 \na+=1 \nprint(a)", "", "exec")
59 lên trên ngăn xếp và
python -m compileall file_1.py ... file_n.py
71 sẽ trả nó cho người gọi của hàm là mô -đun.

Nhưng làm thế nào chức năng này được gọi? Nếu bạn nhìn vào mã byte của dòng 8, trước tiên,

compile("a=5 \na+=1 \nprint(a)", "", "exec")
31
compile("a=5 \na+=1 \nprint(a)", "", "exec")
08 sẽ đẩy đối tượng hàm có tham chiếu là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 lên ngăn xếp.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
70 đẩy đối số của nó (
compile("a=5 \na+=1 \nprint(a)", "", "exec")
71) lên ngăn xếp. Hướng dẫn

compile("a=a+1", "", "single")
8

gọi một đối tượng có thể gọi với các đối số vị trí. OPARG của nó, ARGC chỉ ra số lượng đối số vị trí. Phần trên của ngăn xếp chứa các đối số vị trí, với đối số bên phải trên cùng. Bên dưới các đối số là đối tượng có thể gọi chức năng để gọi.

python -m compileall file_1.py ... file_n.py
77 Đầu tiên bật tất cả các đối số và đối tượng có thể gọi ra khỏi ngăn xếp. Sau đó, nó sẽ phân bổ một khung mới trên ngăn xếp cuộc gọi, điền vào các biến cục bộ cho cuộc gọi hàm và thực thi mã byte của hàm bên trong khung đó. Sau khi hoàn thành, khung sẽ được bật ra khỏi ngăn xếp cuộc gọi và trong khung trước, giá trị trả về của hàm sẽ được đẩy lên trên ngăn xếp đánh giá. Nếu không có khung trước đó, nó sẽ được đẩy lên trên đỉnh đánh giá của khung toàn cầu.

Trong ví dụ của chúng tôi, chúng tôi chỉ có một đối số vị trí, vì vậy hướng dẫn sẽ là

compile("a=5 \na+=1 \nprint(a)", "", "exec")
73. Sau đó, hướng dẫn

compile("a=a+1", "", "single")
9

bật các mặt hàng trên đầu ngăn xếp. Đó là bởi vì chúng tôi không cần giá trị trả về của hàm nữa. Hình 2 cho thấy tất cả các hoạt động bytecode với độ lệch từ 16 đến 22. Các hướng dẫn byte bên trong

compile("a=5 \na+=1 \nprint(a)", "", "exec")
41 được hiển thị màu đỏ.

Hình 2

Chức năng tích hợp sẵn

Trong dòng 9 của mã byte được tháo rời của Danh sách 2, chúng tôi muốn

compile("a=5 \na+=1 \nprint(a)", "", "exec")
75.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
76 cũng là một chức năng, nhưng nó là một hàm Python tích hợp. Tên của hàm là một tham chiếu đến đối tượng có thể gọi của nó. Vì vậy, đầu tiên nó được đẩy lên ngăn xếp và sau đó lập luận của nó được đẩy. Cuối cùng, nó sẽ được gọi bằng cách sử dụng
python -m compileall file_1.py ... file_n.py
77.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
76 sẽ trả về
python -m compileall file_1.py ... file_n.py
31 và giá trị được trả về sẽ được bật ra khỏi ngăn xếp sau đó.

Python sử dụng các chức năng tích hợp của nó để tạo cấu trúc dữ liệu. Ví dụ: dòng sau:

 at 0x000001A1DED95540, file "", line 1>
0

sẽ được chuyển đổi thành:

 at 0x000001A1DED95540, file "", line 1>
1

Ban đầu, mỗi yếu tố của danh sách được đẩy lên ngăn xếp. Sau đó, hướng dẫn

 at 0x000001A1DED95540, file "", line 1>
2

được gọi để tạo danh sách bằng cách sử dụng các mục đếm từ ngăn xếp và đẩy đối tượng danh sách kết quả lên ngăn xếp. Cuối cùng, đối tượng trên ngăn xếp sẽ được bật và lưu trữ trên đống và

compile("a=5 \na+=1 \nprint(a)", "", "exec")
01 sẽ là tài liệu tham khảo của nó.

EXTENDED_ARG

Như đã đề cập trước đây, một số hướng dẫn có thể có một đối số quá lớn để phù hợp với một byte mặc định và chúng sẽ được tiền tố bởi lệnh

python -m compileall file_1.py ... file_n.py
75. Đây là một ví dụ. Giả sử rằng chúng ta muốn in 260
compile("a=5 \na+=1 \nprint(a)", "", "exec")
82 ký tự. Chúng tôi chỉ có thể viết
compile("a=5 \na+=1 \nprint(a)", "", "exec")
83. Tuy nhiên, thay vào đó tôi sẽ viết một cái gì đó bất thường:

 at 0x000001A1DED95540, file "", line 1>
3

Ở đây

compile("a=5 \na+=1 \nprint(a)", "", "exec")
84 chứa hàm
compile("a=5 \na+=1 \nprint(a)", "", "exec")
76 có 260 đối số và mỗi đối số là một ký tự
compile("a=5 \na+=1 \nprint(a)", "", "exec")
82. Bây giờ hãy nhìn vào kết quả được tháo rời mã byte:

 at 0x000001A1DED95540, file "", line 1>
4

Ở đây

compile("a=5 \na+=1 \nprint(a)", "", "exec")
76 được đẩy lên ngăn xếp trước. Sau đó 260 đối số của nó được đẩy. Sau đó
python -m compileall file_1.py ... file_n.py
77 nên gọi chức năng. Nhưng nó cần số lượng đối số (của hàm đích) là oparg của nó. Ở đây con số này là 260 lớn hơn số tối đa mà một byte có thể lấy. Hãy nhớ rằng Oparg chỉ là một byte. Vì vậy,
python -m compileall file_1.py ... file_n.py
77 được có tiền tố bởi
python -m compileall file_1.py ... file_n.py
75. Mã byte thực tế là:

 at 0x000001A1DED95540, file "", line 1>
5

Như đã đề cập trước khi oparg của extends_arg sẽ được dịch chuyển trái bởi tám bit hoặc đơn giản là nhân với 256 và sẽ được thêm vào oparg của opcode tiếp theo. Vì vậy, oparg của

python -m compileall file_1.py ... file_n.py
77 sẽ được hiểu là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
92 (xin lưu ý rằng hàm
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
79 cho thấy điều này được giải thích OPARG không phải là oparg thực tế trong mã byte).

Tuyên bố có điều kiện và nhảy

Xem xét mã nguồn sau đây có câu lệnh

compile("a=5 \na+=1 \nprint(a)", "", "exec")
94:

 at 0x000001A1DED95540, file "", line 1>
6

Mã byte được tháo rời là:

 at 0x000001A1DED95540, file "", line 1>
7

Chúng tôi có một vài hướng dẫn mới ở đây. Trong dòng 2, đối tượng mà

compile("a=5 \na+=1 \nprint(a)", "", "exec")
01 đề cập đến được đẩy lên ngăn xếp, và sau đó nghĩa đen
python -m compileall file_1.py ... file_n.py
67 được đẩy. Hướng dẫn

 at 0x000001A1DED95540, file "", line 1>
8

Thực hiện một hoạt động boolean. Tên hoạt động có thể được tìm thấy trong

compile("a=5 \na+=1 \nprint(a)", "", "exec")
97. Các giá trị của
compile("a=5 \na+=1 \nprint(a)", "", "exec")
98 được lưu trữ trong một danh sách có tên
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
47. Hướng dẫn đầu tiên bật hai yếu tố hàng đầu của ngăn xếp. Chúng tôi gọi cái đầu tiên
compile("a+7", "", "eval")
00 và cái thứ hai
compile("a+7", "", "eval")
01. Sau đó, hoạt động Boolean được chọn bởi Oparg được thực hiện trên chúng
compile("a+7", "", "eval")
02 và kết quả được đẩy lên trên đỉnh của ngăn xếp. Trong ví dụ này
compile("a+7", "", "eval")
03 và
compile("a+7", "", "eval")
04. Ngoài ra, OPARG là
python -m compileall file_1.py ... file_n.py
97 và
compile("a+7", "", "eval")
06. Vì vậy,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
98 sẽ kiểm tra
compile("a+7", "", "eval")
08 và lưu trữ kết quả (đúng hoặc sai) trên đầu ngăn xếp.

Hướng dẫn

 at 0x000001A1DED95540, file "", line 1>
9

Thực hiện một bước nhảy có điều kiện. Đầu tiên, nó bật lên đỉnh của ngăn xếp. Nếu phần tử trên đầu ngăn xếp là sai, nó sẽ đặt bộ đếm bytecode thành mục tiêu. Bộ đếm bytecode hiển thị phần bù mã byte hiện tại đang được thực thi. Vì vậy, nó nhảy đến phần bù bytecode bằng với mục tiêu và việc thực thi mã byte tiếp tục từ đó. Phần bù 18 trong mã byte là một mục tiêu nhảy, do đó có một

compile("a+7", "", "eval")
09 ở phía trước trong đó trong mã byte được tháo rời. Hướng dẫn

a = 5
type(a) # Output is: int
0

tăng bộ đếm bytecode của delta. Trong mã byte trước đó, phần bù của hướng dẫn này là 16 và chúng tôi biết rằng mỗi lệnh có 2 byte. Vì vậy, khi hướng dẫn này kết thúc, bộ đếm bytecode là

compile("a+7", "", "eval")
10. Ở đây
compile("a+7", "", "eval")
11 và
compile("a+7", "", "eval")
12, vì vậy nó nhảy đến phần bù
compile("a+7", "", "eval")
13. Offset 24 là một mục tiêu nhảy và nó cũng có dấu
compile("a+7", "", "eval")
09.

Bây giờ chúng ta có thể thấy cách câu lệnh

compile("a=5 \na+=1 \nprint(a)", "", "exec")
94 được chuyển đổi thành mã byte.
compile("a=5 \na+=1 \nprint(a)", "", "exec")
98 kiểm tra nếu
compile("a+7", "", "eval")
08. Nếu kết quả là sai,
compile("a+7", "", "eval")
18JumpSto, offset 18 là khởi đầu của khối
compile("a+7", "", "eval")
19. Nếu đó là sự thật, khối
compile("a+7", "", "eval")
20 sẽ được thực thi và sau đó
compile("a+7", "", "eval")
21 nhảy đến offset 24 và không thực thi khối
compile("a+7", "", "eval")
19.jumpsto the offset 18 which is the start of
compile("a+7", "", "eval")
19 block. If it is true, the
compile("a+7", "", "eval")
20 block will be executed and then
compile("a+7", "", "eval")
21 jumps to the offset 24 and does not execute the
compile("a+7", "", "eval")
19 block.

Bây giờ, hãy để Lừa thấy một biểu thức Boolean phức tạp hơn. Xem xét mã nguồn sau:

a = 5
type(a) # Output is: int
1

Ở đây chúng tôi có một

compile("a+7", "", "eval")
23 logic. Mã byte được tháo rời là:

a = 5
type(a) # Output is: int
2

Trong Python

compile("a+7", "", "eval")
23 là một toán tử ngắn mạch. Vì vậy, khi đánh giá
compile("a+7", "", "eval")
25, nó chỉ đánh giá
compile("a+7", "", "eval")
26 nếu
compile("a+7", "", "eval")
27 là đúng. Điều này có thể dễ dàng nhìn thấy trong mã byte. Trong dòng 3, đầu tiên, toán hạng bên trái của
compile("a+7", "", "eval")
23 được đánh giá. Nếu
compile("a+7", "", "eval")
29 là sai, nó không đánh giá toán hạng thứ hai và nhảy đến phần bù 30 để thực hiện
compile("a+7", "", "eval")
30Block. Tuy nhiên, nếu đó là sự thật, toán hạng thứ hai
compile("a+7", "", "eval")
31 cũng sẽ được đánh giá.

Vòng lặp và ngăn chặn

Như đã đề cập trước đây, có một ngăn xếp đánh giá bên trong mỗi khung hình. Ngoài ra, trong mỗi khung, có một ngăn xếp khối. Nó được sử dụng bởi CPython để theo dõi các loại cấu trúc điều khiển nhất định như các vòng, các khối

compile("a+7", "", "eval")
32 và các khối
compile("a+7", "", "eval")
33. Khi Cpython muốn nhập một trong các cấu trúc này, một mục mới được đẩy vào ngăn xếp khối và khi CPython thoát ra cấu trúc đó, mục cho cấu trúc đó sẽ bật ra khỏi ngăn xếp khối. Sử dụng ngăn xếp khối Cpython biết cấu trúc nào hiện đang hoạt động. Vì vậy, khi nó đạt đến một tuyên bố
compile("a+7", "", "eval")
34 hoặc
compile("a+7", "", "eval")
35, nó biết cấu trúc nào sẽ bị ảnh hưởng.

Hãy để xem cách các vòng lặp được thực hiện trong mã byte. Xem xét mã sau và mã byte được tháo rời của nó:

a = 5
type(a) # Output is: int
3

Hướng dẫn

a = 5
type(a) # Output is: int
4

được thực hiện trước khi vòng lặp bắt đầu. Lệnh này đẩy một mục mới (còn được gọi là một khối) vào ngăn xếp khối. Delta được thêm vào bộ đếm byte để xác định độ lệch của lệnh tiếp theo sau vòng lặp. Ở đây, phần bù của

compile("a+7", "", "eval")
36 là
python -m compileall file_1.py ... file_n.py
67, do đó bộ đếm bytecode là
compile("a+7", "", "eval")
38. Ngoài ra, Delta là
compile("a+7", "", "eval")
13, do đó, phần bù của lệnh tiếp theo sau vòng lặp là
compile("a+7", "", "eval")
40. Phần bù này được lưu trữ trong khối được đẩy vào ngăn xếp khối. Ngoài ra, số lượng các mục hiện tại trong ngăn xếp đánh giá được lưu trữ trong khối này.

Sau đó, hàm

compile("a+7", "", "eval")
41 nên được thực thi. Oparg của nó (
compile("a=5 \na+=1 \nprint(a)", "", "exec")
09) được đẩy trước tên của hàm. Kết quả là một điều đó. Iterables có thể tạo một trình lặp bằng cách sử dụng hướng dẫn:

a = 5
type(a) # Output is: int
5

Nó có thể đi được trên đỉnh của ngăn xếp và đẩy một người lặp của điều đó. Hướng dẫn:

a = 5
type(a) # Output is: int
6

Giả sử rằng có một người lặp trên đầu ngăn xếp. Nó gọi phương thức

compile("a+7", "", "eval")
43 của nó. Nếu nó mang lại một giá trị mới, giá trị này được đẩy lên trên đầu của ngăn xếp (trên trình lặp). Bên trong vòng lặp, phần trên của ngăn xếp được lưu trữ trong
compile("a+7", "", "eval")
44 sau đó và hàm
compile("a=5 \na+=1 \nprint(a)", "", "exec")
76 được thực thi. Sau đó, phần trên của ngăn xếp là giá trị hiện tại của iterator được bật ra. Sau đó, hướng dẫn

a = 5
type(a) # Output is: int
7

Đặt bộ đếm bytecode thành mục tiêu và nhảy vào phần bù đích. Vì vậy, nó nhảy để bù 10 và chạy lại

compile("a+7", "", "eval")
46 để lấy giá trị tiếp theo của trình lặp. Nếu trình lặp chỉ ra rằng không có phần tử nào có sẵn, thì đỉnh của ngăn xếp được bật ra và bộ đếm mã byte được tăng lên bởi Delta. Ở đây
compile("a+7", "", "eval")
47, vì vậy sau khi hoàn thành vòng lặp, nó nhảy lên để bù 24. Khi bù 24, hướng dẫn

a = 5
type(a) # Output is: int
8

Loại bỏ khối hiện tại khỏi đỉnh của ngăn xếp khối. Phần bù của lệnh tiếp theo sau khi vòng được lưu trữ trong khối (ở đây là 26). Vì vậy, thông dịch viên sẽ nhảy đến phần bù đó và tiếp tục thực hiện từ đó. Hình 3 cho thấy các hoạt động của mã byte với độ lệch 0, 10, 24 và 26 làm ví dụ (trên thực tế trong Hình 1 và 2, chúng tôi chỉ hiển thị ngăn xếp đánh giá trong mỗi khung).

Hình 3

Nhưng điều gì xảy ra nếu chúng ta thêm một tuyên bố

compile("a+7", "", "eval")
34 vào vòng lặp này? Xem xét mã nguồn sau và mã byte được tháo rời của nó:

a = 5
type(a) # Output is: int
9

Chúng tôi chỉ thêm một câu lệnh

compile("a+7", "", "eval")
34 vào vòng lặp trước. Tuyên bố này được chuyển đổi thành

c = compile("a=a+1", "", "single")
type(c) # Output is: code
0

Opcode này loại bỏ các mục bổ sung đó trên ngăn xếp đánh giá và bật khối từ đỉnh của ngăn xếp khối. Bạn nên nhận thấy rằng các hướng dẫn khác của vòng lặp vẫn đang sử dụng ngăn xếp đánh giá. Vì vậy, khi vòng lặp bị vỡ, các mục thuộc về nó nên được bật ra khỏi ngăn xếp đánh giá. Trong ví dụ này, đối tượng Iterator vẫn đứng trên đỉnh của ngăn xếp. Hãy nhớ rằng khối trong ngăn xếp khối lưu trữ số lượng các mục tồn tại trong ngăn xếp đánh giá trước khi bắt đầu vòng lặp.

Vì vậy, bằng cách biết số đó,

compile("a+7", "", "eval")
50pops những mục bổ sung đó khỏi ngăn xếp đánh giá. Sau đó, nó nhảy đến phần bù được lưu trữ trong khối hiện tại của ngăn xếp khối (ở đây là 28). Đó là phần bù của hướng dẫn tiếp theo sau vòng lặp. Vì vậy, vòng lặp phá vỡ và thực hiện được tiếp tục từ đó.pops those extra items off the evaluation stack. Then it jumps to the offset which is stored in the current block of the block stack (here it is 28). That is the offset of the next instruction after the loop. So the loop breaks and the execution is continued from there.

Tạo đối tượng mã

Đối tượng mã là một đối tượng của loại

python -m compileall file_1.py ... file_n.py
46 và có thể tạo nó một cách linh hoạt. Mô -đun
compile("a+7", "", "eval")
52 có thể giúp tạo động các loại mới và lớp
compile("a+7", "", "eval")
53 trong mô -đun này trả về một đối tượng mã mới:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
1

Các đối số tạo thành tất cả các thuộc tính của đối tượng mã. Bạn đã quen thuộc với một số đối số này (như

python -m compileall file_1.py ... file_n.py
92 và
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
54).
compile("a+7", "", "eval")
56 và
compile("a+7", "", "eval")
57 là tùy chọn vì chúng được sử dụng trong các đóng cửa và không phải tất cả các chức năng đều sử dụng chúng (tham khảo bài viết này để biết thêm thông tin về chúng). Các thuộc tính khác được giải thích bằng cách sử dụng hàm sau làm ví dụ:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
2

compile("a+7", "", "eval")
58: Nếu đối tượng mã thuộc hàm, số lượng đối số cần thiết (không bao gồm các đối số từ khóa,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
82 hoặc
compile("a+7", "", "eval")
60 Args). Đối với chức năng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 đó là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
08.

compile("a+7", "", "eval")
63: Nếu đối tượng mã thuộc hàm, số lượng từ khóa chỉ đối số (không bao gồm
compile("a+7", "", "eval")
60 arg). Đối với chức năng
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 đó là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36.

compile("a+7", "", "eval")
67: Số lượng biến cục bộ cộng với tên của các hàm được xác định trong đối tượng mã (đối số cũng được coi là biến cục bộ). Trên thực tế, đó là số lượng các yếu tố trong
python -m compileall file_1.py ... file_n.py
92 là
compile("a+7", "", "eval")
69. Vì vậy, nó là
compile("a+7", "", "eval")
70 cho
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00.

compile("a+7", "", "eval")
72: Hiển thị số lượng phần tử lớn nhất sẽ được đẩy vào ngăn xếp đánh giá bởi đối tượng mã này. Hãy nhớ rằng một số opcode cần phải đẩy một số yếu tố vào ngăn xếp đánh giá. Thuộc tính này cho thấy kích thước lớn nhất mà ngăn xếp sẽ phát triển từ các hoạt động byte. Trong ví dụ này là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
08. Hãy để tôi giải thích lý do cho điều đó. Nếu bạn tháo rời mã byte của chức năng này bạn nhận được:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
3

Trong dòng 2, một phần tử được đẩy lên ngăn xếp bằng cách sử dụng

python -m compileall file_1.py ... file_n.py
65 và sẽ được bật bằng cách sử dụng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
61. Các dòng 5 và 6 tương tự đẩy một phần tử lên ngăn xếp và bật nó sau. Nhưng trong dòng 3, hai phần tử được đẩy lên ngăn xếp để xác định hàm bên trong
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
23: đối tượng mã của nó và tên của nó. Vì vậy, đây là số lượng tối đa các yếu tố sẽ được đẩy vào ngăn xếp đánh giá bởi đối tượng mã này và nó xác định kích thước ngăn xếp.

compile("a+7", "", "eval")
77: Một số nguyên, với các bit chỉ ra những thứ như liệu hàm có chấp nhận số lượng đối số thay đổi hay không, liệu hàm này có phải là trình tạo hay không, v.v. Trong ví dụ của chúng tôi, giá trị của nó là
compile("a+7", "", "eval")
78. Giá trị nhị phân của
compile("a+7", "", "eval")
78 là
compile("a+7", "", "eval")
80. Nó sử dụng một hệ thống ít endian trong đó các byte được viết từ trái bên phải trong việc tăng ý nghĩa. Vì vậy, bit đầu tiên là lần đầu tiên ở bên phải. Bạn có thể tham khảo liên kết này về ý nghĩa của các bit này. Ví dụ: bit thứ ba từ bên phải đại diện cho cờ
compile("a+7", "", "eval")
81. Khi nó là
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36, điều đó có nghĩa là đối tượng mã có tham số vị trí thay đổi (________ 483 giống như).

compile("a+7", "", "eval")
84: Một chuỗi, chỉ định tệp trong đó chức năng có mặt. Trong trường hợp này, đó là
compile("a+7", "", "eval")
85 vì tôi đang chạy tập lệnh trong Jupyter Notebook.

compile("a+7", "", "eval")
86: Một tên mà đối tượng mã này được xác định. Đây là tên của hàm
python -m compileall file_1.py ... file_n.py
99.

Tiêm mã byte

Bây giờ chúng tôi hoàn toàn quen thuộc với đối tượng mã, chúng tôi có thể bắt đầu thay đổi mã byte của nó. Điều quan trọng cần lưu ý là đối tượng mã là bất biến. Vì vậy, một khi được tạo ra, chúng ta không thể thay đổi nó. Giả sử rằng chúng ta muốn thay đổi mã byte của hàm sau:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
4

Ở đây chúng ta không thể thay đổi mã byte của đối tượng mã trực tiếp của hàm. Thay vào đó, chúng ta cần tạo một đối tượng mã mới và sau đó gán nó cho chức năng này. Để làm điều đó, chúng tôi cần thêm một vài chức năng. Hàm

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
79 có thể tháo rời mã byte thành một số hướng dẫn thân thiện với con người. Chúng ta có thể thay đổi chúng như chúng ta muốn, nhưng sau đó chúng ta cần lắp ráp nó trở lại mã byte để gán nó cho một đối tượng mã mới. Đầu ra của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
79 là một chuỗi được định dạng dễ đọc, nhưng khó thay đổi. Vì vậy, tôi sẽ thêm một chức năng mới có thể tháo rời mã byte vào danh sách các hướng dẫn. Nó rất giống với
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
79, tuy nhiên, đầu ra của nó là một danh sách.

Chúng ta có thể thử nó trên chức năng trước đó:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
5

Bây giờ

compile("a+7", "", "eval")
91 bằng với:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
6

Bây giờ chúng ta có thể thay đổi hướng dẫn của danh sách này một cách dễ dàng. Nhưng chúng ta cũng cần lắp ráp nó trở lại mã byte:

Hàm

compile("a+7", "", "eval")
92 giống như nghịch đảo của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
76. Nó có một argvalue là ý nghĩa thân thiện với con người của một oparg và trả về oparg tương ứng. Nó cần đối tượng mã làm đối số của nó vì các thuộc tính của đối tượng mã như
python -m compileall file_1.py ... file_n.py
90 là cần thiết để chuyển đổi argvalue thành oparg.

Hàm

compile("a+7", "", "eval")
95 lấy một đối tượng mã và danh sách mã byte được tháo rời và lắp ráp lại vào mã byte. Nó sử dụng
compile("a+7", "", "eval")
96 để chuyển đổi opname thành opcode. Sau đó, nó gọi
compile("a+7", "", "eval")
97 để chuyển đổi argvalue thành oparg. Cuối cùng, nó trả về một byte theo nghĩa đen của danh sách mã byte. Bây giờ chúng ta có thể sử dụng các chức năng mới này để thay đổi mã byte của hàm trước đó
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00. Đầu tiên, chúng tôi thay đổi một trong các hướng dẫn trong
compile("a+7", "", "eval")
91:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
7

Hướng dẫn

c = compile("a=a+1", "", "single")
type(c) # Output is: code
8

Bật hai yếu tố hàng đầu của ngăn xếp, nhân chúng lại với nhau và đẩy kết quả lên ngăn xếp. Bây giờ chúng tôi lắp ráp mã byte được tháo rời đã sửa đổi:

c = compile("a=a+1", "", "single")
type(c) # Output is: code
9

Sau đó, chúng tôi tạo một đối tượng mã mới:

python -m compileall file_1.py ... file_n.py
00

Chúng tôi sử dụng tất cả các thuộc tính của

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 để tạo nó và chỉ thay thế mã byte mới (
# This does not work:
compile("a=a+1", "", "eval")
01). Sau đó, chúng tôi gán đối tượng mã mới cho
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00. Bây giờ nếu chúng ta chạy lại
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00, nó sẽ không thêm các đối số của nó với nhau. Thay vào đó, nó sẽ nhân chúng lại với nhau:

python -m compileall file_1.py ... file_n.py
01

THẬN TRỌNG: Hàm

# This does not work:
compile("a=a+1", "", "eval")
04 có hai đối số tùy chọn cho
compile("a+7", "", "eval")
56 và ____50506, tuy nhiên, bạn nên cẩn thận khi sử dụng chúng. Như đã đề cập trước các thuộc tính
python -m compileall file_1.py ... file_n.py
93 và
python -m compileall file_1.py ... file_n.py
94 của đối tượng mã chỉ được sử dụng khi đối tượng mã thuộc về một hàm có các biến miễn phí hoặc các biến không thuộc địa. Vì vậy, chức năng nên là một đóng cửa hoặc đóng cửa nên được xác định bên trong nó. Ví dụ, hãy xem xét chức năng sau:
: The
# This does not work:
compile("a=a+1", "", "eval")
04 function has two optional arguments for
compile("a+7", "", "eval")
56 and
# This does not work:
compile("a=a+1", "", "eval")
06, however, you should be careful when using them. As mentioned before the
python -m compileall file_1.py ... file_n.py
93 and
python -m compileall file_1.py ... file_n.py
94 attributes of the code object are only used when the code object belongs to a function which has free variables or nonlocal variables. So the function should be a closure or a closure should have been defined inside it. For example, consider the following function:

python -m compileall file_1.py ... file_n.py
02

Bây giờ nếu kiểm tra đối tượng mã của nó:

python -m compileall file_1.py ... file_n.py
03

Trên thực tế, chức năng này có một biến phi tiêu điểm

s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
25 vì biến này được truy cập bởi các hàm bên trong của nó. Bây giờ chúng ta có thể thử tái tạo lại đối tượng mã của nó bằng cách sử dụng các thuộc tính tương tự:

python -m compileall file_1.py ... file_n.py
04

Nhưng nếu chúng ta kiểm tra cùng một thuộc tính của đối tượng mã mới

python -m compileall file_1.py ... file_n.py
05

Hóa ra là trống rỗng. Vì vậy,

# This does not work:
compile("a=a+1", "", "eval")
04 không thể tạo cùng một đối tượng mã. Nếu bạn cố gắng gán đối tượng mã này cho một hàm và thực thi hàm đó, bạn sẽ gặp lỗi (điều này đã được kiểm tra trên Python 3.7.4).

Tối ưu hóa mã

Hiểu các hướng dẫn bytecode có thể giúp chúng tôi tối ưu hóa mã nguồn. Xem xét mã nguồn sau:

python -m compileall file_1.py ... file_n.py
06

Ở đây chúng tôi xác định một hàm

# This does not work:
compile("a=a+1", "", "eval")
11 để tính toán biểu thức toán học đơn giản. Nó đã được định nghĩa theo hai cách khác nhau. Trong
# This does not work:
compile("a=a+1", "", "eval")
12, chúng tôi đang sử dụng biến toàn cầu
# This does not work:
compile("a=a+1", "", "eval")
13 bên trong
# This does not work:
compile("a=a+1", "", "eval")
11 và trực tiếp sử dụng hàm
# This does not work:
compile("a=a+1", "", "eval")
15 từ mô -đun
# This does not work:
compile("a=a+1", "", "eval")
16. Trong
# This does not work:
compile("a=a+1", "", "eval")
17,
# This does not work:
compile("a=a+1", "", "eval")
13 là một biến cục bộ là
# This does not work:
compile("a=a+1", "", "eval")
11. Ngoài ra,
# This does not work:
compile("a=a+1", "", "eval")
20 lần đầu tiên được lưu trữ trong biến cục bộ
# This does not work:
compile("a=a+1", "", "eval")
21. Bây giờ chúng ta có thể so sánh hiệu suất của các chức năng sau:

python -m compileall file_1.py ... file_n.py
07

Bạn có thể nhận được các số khác nhau cho

# This does not work:
compile("a=a+1", "", "eval")
22 và
# This does not work:
compile("a=a+1", "", "eval")
23, nhưng điểm mấu chốt là
# This does not work:
compile("a=a+1", "", "eval")
17 nhanh hơn
# This does not work:
compile("a=a+1", "", "eval")
12. Bây giờ, hãy để so sánh mã byte của họ để xem tại sao nó nhanh hơn. Chúng tôi chỉ nhìn vào dòng 7 trong mã được tháo rời của
# This does not work:
compile("a=a+1", "", "eval")
12 và
# This does not work:
compile("a=a+1", "", "eval")
17. Đây là mã byte cho dòng này:
# This does not work:
compile("a=a+1", "", "eval")
28.

Trong

# This does not work:
compile("a=a+1", "", "eval")
12 chúng ta có:

python -m compileall file_1.py ... file_n.py
08

Nhưng trong

# This does not work:
compile("a=a+1", "", "eval")
17 chúng tôi nhận được:

python -m compileall file_1.py ... file_n.py
09

Như bạn thấy trong

# This does not work:
compile("a=a+1", "", "eval")
12 cả
# This does not work:
compile("a=a+1", "", "eval")
13 và
# This does not work:
compile("a=a+1", "", "eval")
16 đều được tải bằng
# This does not work:
compile("a=a+1", "", "eval")
34, nhưng trong
# This does not work:
compile("a=a+1", "", "eval")
17,
# This does not work:
compile("a=a+1", "", "eval")
13 và
# This does not work:
compile("a=a+1", "", "eval")
21 được tải bằng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60. Vì vậy, hai hướng dẫn
compile("a=5 \na+=1 \nprint(a)", "", "exec")
62 đã được thay thế bằng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60. Thực tế là
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60 như tên của nó cho thấy nhanh hơn nhiều so với
compile("a=5 \na+=1 \nprint(a)", "", "exec")
62. Chúng tôi đã đề cập rằng tên của các biến toàn cầu và địa phương được lưu trữ trong
python -m compileall file_1.py ... file_n.py
91 và
python -m compileall file_1.py ... file_n.py
92. Nhưng làm thế nào để trình thông dịch CPython tìm thấy các giá trị khi thực thi mã được biên dịch?

Các biến cục bộ được lưu trữ trong một mảng trên mỗi khung (không được hiển thị trong các hình trước để làm cho chúng đơn giản hơn). Chúng tôi biết rằng tên của các biến cục bộ được lưu trữ trong

python -m compileall file_1.py ... file_n.py
92. Giá trị của chúng sẽ được lưu trữ với cùng một thứ tự trong mảng này. Vì vậy, khi trình thông dịch thấy một hướng dẫn như
# This does not work:
compile("a=a+1", "", "eval")
46, nó sẽ đọc phần tử của mảng đó tại INDEX
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
36.

Toàn cầu và tích hợp của mô -đun được lưu trữ trong một từ điển. Chúng tôi biết rằng tên của họ được lưu trữ trong

python -m compileall file_1.py ... file_n.py
91. Vì vậy, khi trình thông dịch thấy một hướng dẫn như
# This does not work:
compile("a=a+1", "", "eval")
49, trước tiên nó có tên của biến toàn cầu đó từ
# This does not work:
compile("a=a+1", "", "eval")
50. Sau đó, nó sẽ tìm kiếm tên này trong từ điển để có được giá trị của nó. Đây là một quá trình chậm hơn nhiều so với việc tìm kiếm mảng đơn giản cho các biến cục bộ. Do đó,
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60 nhanh hơn
compile("a=5 \na+=1 \nprint(a)", "", "exec")
62 và thay thế
compile("a=5 \na+=1 \nprint(a)", "", "exec")
62 bằng
compile("a=5 \na+=1 \nprint(a)", "", "exec")
60 có thể cải thiện hiệu suất. Nó có thể được thực hiện bằng cách lưu trữ các biến tích hợp và toàn cầu thành các biến cục bộ hoặc thay đổi trực tiếp các hướng dẫn mã byte.

Ví dụ: Xác định hằng số trong Python

Ví dụ này minh họa cách sử dụng tiêm mã byte để thay đổi hành vi của các chức năng. Chúng tôi sẽ viết một người trang trí thêm một tuyên bố const cho Python. Trong một số ngôn ngữ lập trình như C, C ++ và JavaScript có một từ khóa Const. Nếu một biến được khai báo là const sử dụng từ khóa này, thì việc thay đổi giá trị của nó là bất hợp pháp và chúng ta không thể thay đổi giá trị của biến này trong mã nguồn nữa.

Python không có tuyên bố const và tôi không cho rằng thực sự cần phải có một từ khóa như vậy trong Python. Ngoài ra, việc xác định hằng số cũng có thể được thực hiện mà không cần sử dụng tiêm mã byte. Vì vậy, đây chỉ là một ví dụ để chỉ cho bạn cách đưa tiêm mã byte vào hành động. Đầu tiên, hãy để tôi chỉ ra cách bạn có thể sử dụng nó. Từ khóa Const được cung cấp bằng cách sử dụng một trình trang trí chức năng có tên

# This does not work:
compile("a=a+1", "", "eval")
55. Khi bạn trang trí một hàm bằng
# This does not work:
compile("a=a+1", "", "eval")
55, bạn có thể khai báo biến bên trong nó dưới dạng hằng số bằng cách sử dụng từ khóa
# This does not work:
compile("a=a+1", "", "eval")
57 (
# This does not work:
compile("a=a+1", "", "eval")
58 ở cuối là một phần của từ khóa). Đây là một ví dụ:

python -m compileall file_1.py ... file_n.py
10

Biến

# This does not work:
compile("a=a+1", "", "eval")
59 bên trong
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 hiện là một hằng số. Bây giờ nếu bạn cố gắng gán lại biến này bên trong
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00, một ngoại lệ sẽ được nêu ra:

python -m compileall file_1.py ... file_n.py
11

Khi một biến được khai báo là Const., Nó nên được gán cho giá trị ban đầu của nó và nó sẽ là một biến cục bộ của hàm đó.

Bây giờ hãy để tôi chỉ cho bạn cách nó đã được thực hiện. Giả sử rằng tôi xác định một chức năng như thế này (không trang trí):

python -m compileall file_1.py ... file_n.py
12

Nó sẽ được biên dịch đúng. Nhưng nếu bạn thử thực thi chức năng này, bạn sẽ gặp lỗi:

python -m compileall file_1.py ... file_n.py
13

Bây giờ chúng ta hãy xem mã byte được tháo rời của chức năng này:

python -m compileall file_1.py ... file_n.py
14

Khi Python cố gắng biên dịch hàm, phải mất

# This does not work:
compile("a=a+1", "", "eval")
55 như một biến toàn cầu vì nó chưa được xác định trong hàm. Biến
# This does not work:
compile("a=a+1", "", "eval")
59 được coi là một thuộc tính của biến toàn cầu
# This does not work:
compile("a=a+1", "", "eval")
59. Trên thực tế,
# This does not work:
compile("a=a+1", "", "eval")
65 giống như
# This does not work:
compile("a=a+1", "", "eval")
66 kể từ khi Python bỏ qua khoảng trắng giữa toán tử DOT và tên của thuộc tính. Tất nhiên, chúng tôi thực sự không có biến toàn cầu có tên
# This does not work:
compile("a=a+1", "", "eval")
59 trong mã nguồn. Nhưng Python sẽ không kiểm tra nó tại thời điểm biên dịch. Chỉ trong quá trình thực hiện, nó sẽ chỉ ra rằng tên
# This does not work:
compile("a=a+1", "", "eval")
55 không được xác định. Vì vậy, mã nguồn của chúng tôi sẽ được chấp nhận trong quá trình biên dịch. Nhưng chúng ta cần thay đổi mã byte của nó trước khi thực thi đối tượng mã của hàm này. Trước tiên chúng ta cần tạo một hàm để thay đổi mã byte:

Hàm này nhận được danh sách các hướng dẫn bytecode được tạo bởi

# This does not work:
compile("a=a+1", "", "eval")
69 làm đối số của nó. Nó có hai danh sách có tên
# This does not work:
compile("a=a+1", "", "eval")
70 và
# This does not work:
compile("a=a+1", "", "eval")
71 lưu trữ tên của các biến được khai báo là Const và lần đầu tiên chúng được chỉ định. Vòng lặp đầu tiên tìm kiếm danh sách các hướng dẫn bytecode và tìm thấy tất cả các hướng dẫn
# This does not work:
compile("a=a+1", "", "eval")
72. Tên của biến phải nằm trong hướng dẫn tiếp theo. Trong ví dụ này, lệnh tiếp theo là
# This does not work:
compile("a=a+1", "", "eval")
73 và tên là
# This does not work:
compile("a=a+1", "", "eval")
59. Tên này và phần bù của hướng dẫn này được lưu trữ trong
# This does not work:
compile("a=a+1", "", "eval")
70 và
# This does not work:
compile("a=a+1", "", "eval")
71. Bây giờ chúng ta cần loại bỏ biến toàn cầu
# This does not work:
compile("a=a+1", "", "eval")
55 và thuộc tính của nó và tạo ra một biến cục bộ có tên
# This does not work:
compile("a=a+1", "", "eval")
59 thay thế. Hướng dẫn

python -m compileall file_1.py ... file_n.py
15

là một mã không có gì. Khi thông dịch viên đạt đến

# This does not work:
compile("a=a+1", "", "eval")
79, nó sẽ bỏ qua nó. Chúng tôi không thể xóa Opcode khỏi danh sách các hướng dẫn vì việc xóa một lệnh làm giảm độ lệch của tất cả các hướng dẫn sau. Bây giờ nếu có một số bước nhảy trong mã byte, phần bù mục tiêu của họ cũng sẽ thay đổi. Vì vậy, việc thay thế hướng dẫn không mong muốn bằng
# This does not work:
compile("a=a+1", "", "eval")
79 sẽ dễ dàng hơn nhiều. Bây giờ chúng tôi thay thế
# This does not work:
compile("a=a+1", "", "eval")
72 bằng
# This does not work:
compile("a=a+1", "", "eval")
79 và sau đó thay thế
# This does not work:
compile("a=a+1", "", "eval")
73 bằng
# This does not work:
compile("a=a+1", "", "eval")
84. Mã byte cuối cùng trông như thế này:

python -m compileall file_1.py ... file_n.py
16

Bây giờ dòng 2 tương đương với

# This does not work:
compile("a=a+1", "", "eval")
85 trong mã nguồn và việc thực thi mã byte này không gây ra bất kỳ lỗi thời gian chạy nào. Vòng lặp cũng kiểm tra xem cùng một biến không được khai báo là const hai lần. Vì vậy, nếu biến được khai báo là const đã tồn tại trong danh sách
# This does not work:
compile("a=a+1", "", "eval")
70, nó sẽ tăng một ngoại lệ tùy chỉnh. Bây giờ điều duy nhất còn lại là đảm bảo rằng các biến const chưa được chỉ định lại.

Vòng lặp thứ hai tìm kiếm danh sách các hướng dẫn bytecode một lần nữa để tìm bất kỳ sự phân công lại của các biến không đổi. Bất kỳ hướng dẫn nào như

# This does not work:
compile("a=a+1", "", "eval")
87 hoặc
# This does not work:
compile("a=a+1", "", "eval")
84 đều có nghĩa là việc tái chỉ định trong mã nguồn, do đó, nó sẽ tăng một ngoại lệ tùy chỉnh để cảnh báo người dùng. Sự bù đắp của nhiệm vụ ban đầu của một const là bắt buộc để đảm bảo rằng nhiệm vụ ban đầu không được coi là một sự phân công lại.

Như đã đề cập trước đó, mã byte nên được thay đổi trước khi thực thi mã. Vì vậy, hàm

# This does not work:
compile("a=a+1", "", "eval")
89 cần được gọi trước khi gọi hàm
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00. Vì lý do này, chúng tôi đặt nó bên trong một người trang trí. Hàm trang trí
# This does not work:
compile("a=a+1", "", "eval")
55 nhận chức năng đích
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 làm đối số của nó. Trước tiên, nó sẽ thay đổi mã byte của
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00 bằng cách sử dụng
# This does not work:
compile("a=a+1", "", "eval")
89 và sau đó tạo một đối tượng mã mới với mã byte được sửa đổi. Đối tượng mã này sẽ được gán cho
s='''
a=5
a+=1
print(a)
'''
compile(s, "", "exec")
00.

Khi chúng ta tạo đối tượng mã mới, một số thuộc tính của nó cần phải được sửa đổi. Trong hàm ban đầu

# This does not work:
compile("a=a+1", "", "eval")
55 là một biến toàn cầu và
# This does not work:
compile("a=a+1", "", "eval")
59 là một thuộc tính, do đó, cả hai đều được thêm vào tuple
python -m compileall file_1.py ... file_n.py
91 và chúng nên được xóa khỏi
python -m compileall file_1.py ... file_n.py
91 của đối tượng mã mới. Ngoài ra, khi một thuộc tính như
# This does not work:
compile("a=a+1", "", "eval")
59 được biến thành một biến cục bộ, tên của nó phải được thêm vào tuple
python -m compileall file_1.py ... file_n.py
92. Thuộc tính
compile("a+7", "", "eval")
67 cung cấp số lượng biến cục bộ (cộng với các hàm được xác định) và cũng nên được cập nhật. Các thuộc tính khác vẫn giữ nguyên. Trình trang trí cuối cùng trả về chức năng đích với đối tượng mã mới và bây giờ chức năng đích đã sẵn sàng để thực thi.

Hiểu mã byte của Python, cho phép bạn làm quen với việc triển khai cấp thấp của trình biên dịch Python và máy ảo. Nếu bạn biết cách chuyển đổi mã nguồn của bạn thành mã byte, bạn có thể đưa ra quyết định tốt hơn về việc viết và tối ưu hóa mã của bạn. Tiêm Bytecode cũng là một công cụ hữu ích để tối ưu hóa mã và siêu hình. Tôi chỉ đề cập đến một số lượng nhỏ các hướng dẫn mã byte trong bài viết này. Bạn có thể tham khảo trang web mô -đun

python -m compileall file_1.py ... file_n.py
57 để xem danh sách đầy đủ các hướng dẫn mã byte Python. Tôi hy vọng rằng bạn đã thích đọc bài viết này. Tất cả các danh sách mã của bài viết này đều có sẵn để tải xuống dưới dạng Jupyter Notebook tại: https://github.com/reza-bagheri/undering-python-bytecode

Làm thế nào để bạn viết mã byte trong Python?

Mã byte được tự động tạo trong cùng một thư mục với tệp .py, khi một mô -đun Python lần đầu tiên được nhập hoặc khi nguồn gần đây hơn tệp được biên dịch hiện tại. Lần tới, khi chương trình được chạy, người thông báo Python sử dụng tệp này để bỏ qua bước biên dịch. py file, when a module of python is imported for the first time, or when the source is more recent than the current compiled file. Next time, when the program is run, python interpretator use this file to skip the compilation step.

Làm thế nào để Python thực thi mã byte?

Việc triển khai mặc định của ngôn ngữ lập trình Python là CPython được viết bằng ngôn ngữ lập trình C. Cpython biên dịch mã nguồn Python vào mã byte và mã byte này sau đó được thực thi bởi máy ảo CPython. Tất cả các tệp PYC được tạo sẽ được lưu trữ trong thư mục __pycache__.by the CPython virtual machine. All the generated pyc files will be stored in the __pycache__ folder.

Tại sao Bytecode được sử dụng trong Python?

Bytecode là ngôn ngữ trung gian cho máy ảo Python được sử dụng làm tối ưu hóa hiệu suất.as a performance optimization.

Làm thế nào bytecode được thực thi?

Chấp hành.Một chương trình ByteCode có thể được thực thi bằng cách phân tích cú pháp và thực hiện trực tiếp các hướng dẫn, mỗi lần một.Loại thông dịch viên byte này rất di động.Một số hệ thống, được gọi là Trình dịch động hoặc trình biên dịch chỉ trong thời gian (JIT), dịch mã byte thành mã máy khi cần thiết trong thời gian chạy.by parsing and directly executing the instructions, one at a time. This kind of bytecode interpreter is very portable. Some systems, called dynamic translators, or just-in-time (JIT) compilers, translate bytecode into machine code as necessary at runtime.