我們每天都要寫一些Python程序,或用來處理一些文本,或是做一些系統管理工作。程式寫好後,只需敲下python指令,便可將程式啟動起來並開始執行:
$ python some-program.py
那麼,一個文字形式的.py文件,是如何一步步轉換為能夠被CPU執行的機器指令的呢?此外,程式執行過程中可能會有.pyc檔生成,這些檔又有什麼作用呢?
雖然從行為上看Python更像Shell腳本這樣的解釋性語言,但實際上Python程式執行原理本質上跟Java或者C#一樣,都可以歸納為虛擬機器和字節碼。 Python執行程式分成兩個步驟:先將程式碼編譯成字節碼,然後啟動虛擬機器執行字節碼:
#雖然Python指令也叫做Python解釋器,但跟其他腳本語言解釋器有本質差異。實際上,Python解釋器包含編譯器以及虛擬機器兩部分。當Python解釋器啟動後,主要執行以下兩個步驟:
編譯器將.py檔案中的Python原始碼編譯成字節碼虛擬機器逐行執行編譯器產生的字節碼
因此,.py檔案中的Python語句並沒有直接轉換成機器指令,而是轉換成Python字節碼。
Python程式的編譯結果是字節碼,裡面有很多關於Python運行的相關內容。因此,不管是為了更深入理解Python虛擬機器運作機制,還是為了調優Python程式運作效率,字節碼都是關鍵內容。那麼,Python字節碼到底長啥樣呢?我們如何才能獲得一個Python程式的字節碼呢——Python提供了一個內建函數compile用於即時編譯原始碼。我們只需將待編譯原始碼作為參數呼叫compile函數,即可獲得原始碼的編譯結果。
下面,我們透過compile函數來編譯一個程式:
原始碼保存在demo.py檔中:
PI = 3.14 def circle_area(r): return PI * r ** 2 class Person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
編譯之前需要將原始碼從檔案中讀取出來:
>>> text = open('D:\myspace\code\pythonCode\mix\demo.py').read() >>> print(text) PI = 3.14 def circle_area(r): return PI * r ** 2 class Person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
然後呼叫compile函數來編譯原始碼:
>>> result = compile(text,'D:\myspace\code\pythonCode\mix\demo.py', 'exec')
compile函數必填的參數有3個:
#source :待編譯原始碼
filename:原始碼所在檔案名稱
mode:編譯模式,exec表示將原始碼當作一個模組來編譯
exec:用於編譯模組原始碼
single:用於編譯一個單獨的Python語句(互動下)
eval:用於編譯一個eval表達式
透過compile函數,我們得到了最後的原始碼編譯結果result:
>>> result <code object <module> at 0x000001DEC2FCF680, file "D:\myspace\code\pythonCode\mix\demo.py", line 1> >>> result.__class__ <class 'code'>
最終我們得到了一個code類型的對象,它對應的底層結構體是PyCodeObject
PyCodeObject原始碼如下:
/* Bytecode object */ struct PyCodeObject { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_posonlyargcount; /* #positional only arguments */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ int co_firstlineno; /* first source line number */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest aren't used in either hash or comparisons, except for co_name, used in both. This is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ PyObject *co_linetable; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ /* Scratch space for extra data relating to the code object. Type is a void* to keep the format private in codeobject.c to force people to go through the proper APIs. */ void *co_extra; /* Per opcodes just-in-time cache * * To reduce cache size, we use indirect mapping from opcode index to * cache object: * cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1] */ // co_opcache_map is indexed by (next_instr - first_instr). // * 0 means there is no cache for this opcode. // * n > 0 means there is cache in co_opcache[n-1]. unsigned char *co_opcache_map; _PyOpcache *co_opcache; int co_opcache_flag; // used to determine when create a cache. unsigned char co_opcache_size; // length of co_opcache. };
程式碼物件PyCodeObject用於儲存編譯結果,包括字節碼以及程式碼涉及的常數、名字等等。關鍵字段包括:
字段 | 用途 |
---|---|
co_argcount | 參數個數 |
co_kwonlyargcount | #關鍵字參數數量 |
co_nlocals | #局部變數個數 |
co_stacksize | 執行程式碼所需堆疊空間 |
co_flags | ##標識|
程式碼區塊首行號 | |
指令運算子,即字節碼 | |
#常數清單 | |
名字清單 | |
下面打印看一下这些字段对应的数据:
通过co_code字段获得字节码:
>>> result.co_code b'd\x00Z\x00d\x01d\x02\x84\x00Z\x01G\x00d\x03d\x04\x84\x00d\x04e\x02\x83\x03Z\x03d\x05S\x00'
通过co_names字段获得代码对象涉及的所有名字:
>>> result.co_names ('PI', 'circle_area', 'object', 'Person')
通过co_consts字段获得代码对象涉及的所有常量:
>>> result.co_consts (3.14, <code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>, 'circle_area', <code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>, 'Person', None)
可以看到,常量列表中还有两个代码对象,其中一个是circle_area函数体,另一个是Person类定义体。对应Python中作用域的划分方式,可以自然联想到:每个作用域对应一个代码对象。如果这个假设成立,那么Person代码对象的常量列表中应该还包括两个代码对象:init函数体和say函数体。下面取出Person类代码对象来看一下:
>>> person_code = result.co_consts[3] >>> person_code <code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6> >>> person_code.co_consts ('Person', <code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>, 'Person.__init__', <code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>, 'Person.say', None)
因此,我们得出结论:Python源码编译后,每个作用域都对应着一个代码对象,子作用域代码对象位于父作用域代码对象的常量列表里,层级一一对应。
至此,我们对Python源码的编译结果——代码对象PyCodeObject有了最基本的认识,后续会在虚拟机、函数机制、类机制中进一步学习。
字节码是一串不可读的字节序列,跟二进制机器码一样。如果想读懂机器码,可以将其反汇编,那么字节码可以反编译吗?
通过dis模块可以将字节码反编译:
>>> import dis >>> dis.dis(result.co_code) 0 LOAD_CONST 0 (0) 2 STORE_NAME 0 (0) 4 LOAD_CONST 1 (1) 6 LOAD_CONST 2 (2) 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (1) 12 LOAD_BUILD_CLASS 14 LOAD_CONST 3 (3) 16 LOAD_CONST 4 (4) 18 MAKE_FUNCTION 0 20 LOAD_CONST 4 (4) 22 LOAD_NAME 2 (2) 24 CALL_FUNCTION 3 26 STORE_NAME 3 (3) 28 LOAD_CONST 5 (5) 30 RETURN_VALUE
字节码反编译后的结果和汇编语言很类似。其中,第一列是字节码的偏移量,第二列是指令,第三列是操作数。以第一条字节码为例,LOAD_CONST指令将常量加载进栈,常量下标由操作数给出,而下标为0的常量是:
>>> result.co_consts[0]3.14
这样,第一条字节码的意义就明确了:将常量3.14加载到栈。
由于代码对象保存了字节码、常量、名字等上下文信息,因此直接对代码对象进行反编译可以得到更清晰的结果:
>>>dis.dis(result) 1 0 LOAD_CONST 0 (3.14) 2 STORE_NAME 0 (PI) 3 4 LOAD_CONST 1 (<code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>) 6 LOAD_CONST 2 ('circle_area') 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (circle_area) 6 12 LOAD_BUILD_CLASS 14 LOAD_CONST 3 (<code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>) 16 LOAD_CONST 4 ('Person') 18 MAKE_FUNCTION 0 20 LOAD_CONST 4 ('Person') 22 LOAD_NAME 2 (object) 24 CALL_FUNCTION 3 26 STORE_NAME 3 (Person) 28 LOAD_CONST 5 (None) 30 RETURN_VALUE Disassembly of <code object circle_area at 0x0000023D04D3F310, file "D:\myspace\code\pythonCode\mix\demo.py", line 3>: 4 0 LOAD_GLOBAL 0 (PI) 2 LOAD_FAST 0 (r) 4 LOAD_CONST 1 (2) 6 BINARY_POWER 8 BINARY_MULTIPLY 10 RETURN_VALUE Disassembly of <code object Person at 0x0000023D04D3F5D0, file "D:\myspace\code\pythonCode\mix\demo.py", line 6>: 6 0 LOAD_NAME 0 (__name__) 2 STORE_NAME 1 (__module__) 4 LOAD_CONST 0 ('Person') 6 STORE_NAME 2 (__qualname__) 7 8 LOAD_CONST 1 (<code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>) 10 LOAD_CONST 2 ('Person.__init__') 12 MAKE_FUNCTION 0 14 STORE_NAME 3 (__init__) 10 16 LOAD_CONST 3 (<code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>) 18 LOAD_CONST 4 ('Person.say') 20 MAKE_FUNCTION 0 22 STORE_NAME 4 (say) 24 LOAD_CONST 5 (None) 26 RETURN_VALUE Disassembly of <code object __init__ at 0x0000023D04D3F470, file "D:\myspace\code\pythonCode\mix\demo.py", line 7>: 8 0 LOAD_FAST 1 (name) 2 LOAD_FAST 0 (self) 4 STORE_ATTR 0 (name) 6 LOAD_CONST 0 (None) 8 RETURN_VALUE Disassembly of <code object say at 0x0000023D04D3F520, file "D:\myspace\code\pythonCode\mix\demo.py", line 10>: 11 0 LOAD_GLOBAL 0 (print) 2 LOAD_CONST 1 ('i am') 4 LOAD_FAST 0 (self) 6 LOAD_ATTR 1 (name) 8 CALL_FUNCTION 2 10 POP_TOP 12 LOAD_CONST 0 (None) 14 RETURN_VALUE
操作数指定的常量或名字的实际值在旁边的括号内列出,此外,字节码以语句为单位进行了分组,中间以空行隔开,语句的行号在字节码前面给出。例如PI = 3.14这个语句就被会变成了两条字节码:
1 0 LOAD_CONST 0 (3.14) 2 STORE_NAME 0 (PI)
如果将demo作为模块导入,Python将在demo.py文件所在目录下生成.pyc文件:
>>> import demo
pyc文件会保存经过序列化处理的代码对象PyCodeObject。这样一来,Python后续导入demo模块时,直接读取pyc文件并反序列化即可得到代码对象,避免了重复编译导致的开销。只有demo.py有新修改(时间戳比.pyc文件新),Python才会重新编译。
因此,对比Java而言:Python中的.py文件可以类比Java中的.java文件,都是源码文件;而.pyc文件可以类比.class文件,都是编译结果。只不过Java程序需要先用编译器javac命令来编译,再用虚拟机java命令来执行;而Python解释器把这两个过程都完成了。
以上是Python程式的執行過程包括將原始碼轉換為字節碼(即編譯)以及執行字節碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!