Python程序的執行過程
我們都知道,C語言在執行之前需要將源代碼編譯成可執行的二進制文件,也就是將源代碼翻譯成機器代碼,這種二進制文件一旦生成,即可用於執行。但是,Python是否一樣呢?或許很多人都聽過,Python和Java都是半編譯半解釋的語言,那么問題來了,什么又是半編譯半解釋呢?這還要從C語言開始說起
比方我們現在有一段C語言寫成的程序,我們在一台Linux服務器上編譯好了,生成可執行的二進制文件,可是我現在想要在一台Windows的機器上執行這個文件,這是不可能的,原因是因為不同平台間的機器代碼是不一樣的,在Linux機器上生成的二進制可執行文件,是不能拿到Windows上執行的,甚至都是在Linux上編譯的文件,但是用的C編譯器不同,一樣有可能無法執行。所以,這才有了半編譯半解釋。
半編譯半解釋保證,一次編譯,到處運行。這是Java的承諾,同樣適用於Python。半編譯半解釋會從源代碼中產生一組字節碼,它並不是機器代碼,但是不管是在Linux還是Windows的機器上,同樣的源代碼產生的字節碼都是一樣的,同時它還有個虛擬機,虛擬機會一條一條執行字節碼,生成可執行的機器代碼交給CPU執行。正是因為字節碼和虛擬機這兩個特性,使得我們的程序可以正常執行在Linux或Windows機器上
那么你一定好奇,Python的編譯器和Python的虛擬機在什么地方呢?於是,我們來到安裝Python的目錄下,首先我們看到的是python.exe,於是我們懷疑python.exe是不是編譯器或者虛擬機其中一個,不過我們發現,python.exe只有92KB,似乎並不大,不太可能支撐起一個龐大的語言。
實際上,Python編譯器和Python虛擬機都在同一個文件里,而且還在上面最大的文件里,沒錯,就是python25.dll這個文件,這個文件既要完成編譯工作,同時還要完成解釋工作(即虛擬機的工作)
熟悉Java的同學都知道(不熟悉也沒關系,舉個栗子而已),Java在編譯程序的時候,會產生一個class文件,最后調用Java命令執行class文件中的字節碼。而Python作為同樣的半編譯半解釋的語言,也有類似的特性,Python執行的時候,有可能會產生一個字節碼文件,注意,只是有可能,而Java是一定會產生字節碼文件。Python既可以直接執行源代碼文件,也可以執行字節碼文件,何時會產生這個字節碼文件,我們且往后看
PyCodeObject對象與pyc文件
剛剛我們說,Python在執行一個文件時,是有可能產生字節碼文件的,那么是在何時才會產生呢?我們可以寫一個demo.py,里面隨便你寫什么程序,然后直接用python執行,會發現,不管我們怎么執行,都不會生成pyc文件。嗯……看來是我們的操作有點問題?那什么才是生成pyc文件的正確操作呢?
當然是當我們執行一個腳本時,腳本引入的模塊會產生pyc文件了!
這樣說可能還有些人不懂,沒關系,我們直接上代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# ls
demo.py
# cat demo.py
class
A:
pass
def func():
pass
a = A()
func()
|
從上面的代碼我們可以看到,在當前目錄下只有一個demo.py的文件,並且我們打印出這個文件的內容,當然,這個內容不重要,怎么生成pyc文件最重要。我們再來看下面的代碼,我們在當前的目錄直接進入python命令行,然后引入demo模塊再退出
1
2
3
4
5
6
|
# python
…………
>>>
from
demo
import
*
>>>
# ls
demo.py demo.pyc
|
然后我們再打印當前目錄下的文件,神奇的事發生了,多了一個demo.pyc文件。
為什么當引入一個Python文件時,這個Python文件對應的pyc文件會生成呢?可以這樣認為,當一個文件被引入時,代表這個文件很可能是要經常被引用的,因此Python編譯這個文件,生成字節碼。而僅僅是執行一個腳本,Python會認為這個腳本只執行一次,后續不會再執行,所以,不會為其生成pyc文件
pyc文件中,存儲着一個PyCodeObject對象,對於Python編譯器來說,PyCodeObject對象才是真正的編譯結果,而pyc文件只是這個對象在磁盤上的表現形式,它們其實是Python對源文件編譯的結果的兩種不同的存在方式。程序運行期間,編譯結果存在於內存中的PyCodeObject對象中,而程序結束后,編譯結果又被保存到pyc文件中,當下一次運行相同程序時,Python會根據pyc文件中記錄的編譯結果,直接在內存中建立PyCodeObject對象,而不用再次編譯
PyCodeObject對象
我們先來看Python源碼中關於PyCodeObject的聲明:
code.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/
*
Bytecode
object
*
/
typedef struct {
PyObject_HEAD
int
co_argcount;
/
*
#arguments, except *args */
int
co_nlocals;
/
*
#local variables */
int
co_stacksize;
/
*
#entries needed for evaluation stack */
int
co_flags;
/
*
CO_..., see below
*
/
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 doesn't count
for
hash
/
cmp
*
/
PyObject
*
co_filename;
/
*
string (where it was loaded
from
)
*
/
PyObject
*
co_name;
/
*
string (name,
for
reference)
*
/
int
co_firstlineno;
/
*
first source line number
*
/
PyObject
*
co_lnotab;
/
*
string (encoding addr<
-
>lineno mapping)
*
/
void
*
co_zombieframe;
/
*
for
optimization only (see frameobject.c)
*
/
} PyCodeObject;
|
PyCodeObject對象中的各個域所包含的信息我們會在下面一步一步挖掘開來。Python編譯器在對Python源代碼進行編譯的時候,對於代碼中的一個Code Block,會創建一個PyCodeObject對象與這段代碼對應,那么,在代碼中,怎么樣才算是一個Code Block呢?Python中是這樣定義的:當進入一個新的名字空間,或者說作用域時,我們算是進入一個新的Code Block了。
回顧一下上面的demo.py文件,Python編譯器對源代碼完成編譯后,總共會創建3個PyCodeObject對象,一個是對應demo.py整個文件的,一個是對應class A所代表的Code Block,最后一個是def func所代表的Code Block
這里,我們提及Python中一個至關重要的概念——名字空間,名字空間是符號的上下文環境,符號的含義取決於名字空間。更具體的說,一個變量名對應的變量值是什么,在Python中是不確定的,而是通過名字空間來決定的
對於某個變量名,比如a,在同一個腳本中,可能在某一個類里,它代表的是一串字符串,而在另外一個方法中,它代表的是一個整型值,但a這個變量到底是什么值,就是由名字空間來決定的。名字空間可以一個套一個地形成一條名字空間鏈,Python虛擬機在執行的過程中,會有很大一部分時間消耗在從這條名字空間鏈中確定一個符號所對應的對象是什么
正如我們前面所說,一個Code Block就對應一個名字空間,即對應一個PyCodeObject對象。在Python中,類、函數、module都對應着一個獨立的名字空間。因此,都會有一個PyCodeObject對象與其對應。所以,demo.py經過Python編譯器編譯后,一共得到3個PyCodeObject對象
pyc文件
每一個PyCodeObject對象都包含了每一個Code Block中所有Python源代碼經過編譯后得到的byte code序列,Python會將這些字節碼序列和PyCodeObject對象一起存儲在pyc文件中。要了解pyc文件,首先我們必須清楚PyCodeObject中大部分域所代表的含義,這一點是無論如何都不能繞過去的
Field | Content |
co_argcount | Code Block的位置參數個數,比如說一個函數的位置參數個數 |
co_nlocals | Code Block中局部變量的個數,包括其位置參數的個數 |
co_stacksize | 執行該段Code Block需要的棧空間 |
co_flags | N/A,表示該域對理解Python虛擬機的行為沒太多用處 |
co_code | Code Block編譯所得的字節碼指令序列,以PyStringObject的形式存在 |
co_consts | PyTuppleObject對象,保存Code Block中所有的常量 |
co_names | PyTuppleObject對象,保存Code Block中所有的符號 |
co_varnames | Code Block中的局部變量名集合 |
co_freevars | Python實現閉包需要用到的東西,為自由變量 |
co_cellvars | Code Block中內部嵌套函數所引用的局部變量名集合 |
co_filename | Code Block所對應的.py文件的完整路徑 |
co_name | Code Block的名字,通常是函數名或類名 |
co_firstlineno | Code Block在對應的.py文件中的起始行 |
co_lnotab | 字節碼指令與.py文件中的source code行號的對應關系,以PyStringObject的形式存在 |
co_lnotab中的字節碼和相應的source code行號的對應信息是以unsigned bytes的數組形式存在的,數組的形式可以看作(字節碼指令在co_code中位置,source code行號)形式的一個list,如下面的表格:
表1-1 | |
字節碼在co_code中的偏移 | .py文件中源代碼的行號 |
0 | 1 |
6 | 2 |
50 | 7 |
這里有個小技巧,Python不會直接記錄這些信息,但是它會記錄這些信息間的增量值。所以,對應的co_lnotab應該如下表:
表1-2 | |
字節碼在co_code中的偏移 | .py文件中源代碼的行號 |
0 | 1 |
6(6+0=6) | 1(1+1=2) |
44(44+6+0=50) | 5(5+1+1=7) |
在Python中訪問PyCodeObject對象
在Python中,有與C一級的PyCodeObject對象對應的對象——code對象,這個對象是對C一級的PyCodeObject對象的一個簡單包裝,通過code對象,我們可以訪問到PyCodeObject對象中的各個域
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> source
=
open
(
"demo.py"
).read()
>>> co
=
compile
(source,
"demo.py"
,
"exec"
)
>>>
type
(co)
<
class
'code'
>
>>>
dir
(co)
[
'__class__'
,
'__delattr__'
,
'__dir__'
,
'__doc__'
,
'__eq__'
,
'__format__'
,
'__ge__'
,
'__getattribute__'
,
'__gt__'
,
'__hash__'
,
'__init__'
,
'__init_subclass__'
,
'__le__'
,
'__lt__'
,
'__ne__'
,
'__new__'
,
'__reduce__'
,
'__reduce_ex__'
,
'__repr__'
,
'__setattr__'
,
'__sizeof__'
,
'__str__'
,
'__subclasshook__'
,
'co_argcount'
,
'co_cellvars'
,
'co_code'
,
'co_consts'
,
'co_filename'
,
'co_firstlineno'
,
'co_flags'
,
'co_freevars'
,
'co_kwonlyargcount'
,
'co_lnotab'
,
'co_name'
,
'co_names'
,
'co_nlocals'
,
'co_stacksize'
,
'co_varnames'
]
>>> co.co_names
(
'A'
,
'func'
,
'a'
)
>>> co.co_name
'<module>'
>>> co.co_filename
'demo.py'
|