http://www.restran.net/2015/10/22/how-python-code-run/
這篇文章準確說是『Python 源碼剖析』的讀書筆記,整理完之后才發現很長,那就將就看吧。以下使用的是 Python 2,在 Python 3 下一些細節可能會略有不同。
## [](http://www.restran.net/2015/10/22/how-python-code-run/#1-簡單的例子 "1\. 簡單的例子")1\. 簡單的例子
先從一個簡單的例子說起,包含了兩個文件 foo.py 和 demo.py
|
1
2
3
|
[foo.py]
def add(a, b):
return a + b
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[demo.py]
import foo
a = [1, 'python']
a = 'a string'
def func():
a = 1
b = 257
print(a + b)
print(a)
if __name__ == '__main__':
func()
foo.add(1, 2)
|
執行這個程序
~~~
python demo.py
~~~
輸出結果
~~~
a string
258
~~~
同時,該文件目錄多出一個 foo.pyc 文件,如果是 Python 3,會多出一個?`__pycache__`?文件夾,`.pyc`?文件會在這個文件夾中。
## [](http://www.restran.net/2015/10/22/how-python-code-run/#2-背后的魔法 "2\. 背后的魔法")2\. 背后的魔法
看完程序的執行結果,接下來開始一行行解釋代碼。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-1-模塊 "2.1 模塊")2.1 模塊
Python 將 .py 文件視為一個 module,這些 module 中,有一個主 module,也就是程序運行的入口。在這個例子中,主 module 是 demo.py。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-2-編譯 "2.2 編譯")2.2 編譯
執行?`python demo.py`?后,將會啟動 Python 的解釋器,然后將 demo.py 編譯成一個字節碼對象 PyCodeObject。
有的人可能會很好奇,編譯的結果不應是 pyc 文件嗎,就像 Java 的 class 文件,那為什么是一個對象呢,這里稍微解釋一下。
> 在 Python 的世界中,一切都是對象,函數也是對象,類型也是對象,類也是對象(類屬于自定義的類型,在 Python 2.2 之前,int, dict 這些內置類型與類是存在不同的,在之后才統一起來,全部繼承自 object),甚至連編譯出來的字節碼也是對象,.pyc 文件是字節碼對象(PyCodeObject)在硬盤上的表現形式。
在運行期間,編譯結果也就是 PyCodeObject 對象,只會存在于內存中,而當這個模塊的 Python 代碼`執行完`后,就會將編譯結果保存到了 pyc 文件中,這樣下次就不用編譯,直接加載到內存中。pyc 文件只是 PyCodeObject 對象在硬盤上的表現形式。
這個 PyCodeObject 對象包含了 Python 源代碼中的字符串,常量值,以及通過語法解析后編譯生成的字節碼指令。PyCodeObject 對象還會存儲這些字節碼指令與原始代碼行號的對應關系,這樣當出現異常時,就能指明位于哪一行的代碼。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-3-pyc-文件 "2.3 pyc 文件")2.3 pyc 文件
一個 pyc 文件包含了三部分信息:Python 的 magic number、pyc 文件創建的時間信息,以及 PyCodeObject 對象。
magic number 是 Python 定義的一個整數值。一般來說,不同版本的 Python 實現都會定義不同的 magic number,這個值是用來保證 Python 兼容性的。比如要限制由低版本編譯的 pyc 文件不能讓高版本的 Python 程序來執行,只需要檢查 magic number 不同就可以了。由于不同版本的 Python 定義的字節碼指令可能會不同,如果不做檢查,執行的時候就可能出錯。
下面所示的代碼可以來創建 pyc 文件,使用方法
~~~
python generate_pyc.py module_name
~~~
例如
~~~
python generate_pyc.py demo
~~~
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[generate_pyc.py]
import imp
import sys
def generate_pyc(name):
fp, pathname, description = imp.find_module(name)
try:
imp.load_module(name, fp, pathname, description)
finally:
if fp:
fp.close()
if __name__ == '__main__':
generate_pyc(sys.argv[1])
|
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-4-字節碼指令 "2.4 字節碼指令")2.4 字節碼指令
> 為什么 pyc 文件也稱作字節碼文件?因為這些文件存儲的都是一些二進制的字節數據,而不是能讓人直觀查看的文本數據。
Python 標準庫提供了用來生成代碼對應字節碼的工具?`dis`。dis 提供一個名為 dis 的方法,這個方法接收一個 code 對象,然后會輸出 code 對象里的字節碼指令信息。
|
1
2
3
4
|
s = open('demo.py').read()
co = compile(s, 'demo.py', 'exec')
import dis
dis.dis(co)
|
執行上面這段代碼可以輸出 demo.py 編譯后的字節碼指令
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
1 0 LOAD_CONST 0 (-1)
3 LOAD_CONST 1 (None)
6 IMPORT_NAME 0 (foo)
9 STORE_NAME 0 (foo)
3 12 LOAD_CONST 2 (1)
15 LOAD_CONST 3 (u'python')
18 BUILD_LIST 2
21 STORE_NAME 1 (a)
4 24 LOAD_CONST 4 (u'a string')
27 STORE_NAME 1 (a)
6 30 LOAD_CONST 5 ()
33 MAKE_FUNCTION 0
36 STORE_NAME 2 (func)
11 39 LOAD_NAME 1 (a)
42 PRINT_ITEM
43 PRINT_NEWLINE
13 44 LOAD_NAME 3 (__name__)
47 LOAD_CONST 6 (u'__main__')
50 COMPARE_OP 2 (==)
53 POP_JUMP_IF_FALSE 82
14 56 LOAD_NAME 2 (func)
59 CALL_FUNCTION 0
62 POP_TOP
15 63 LOAD_NAME 0 (foo)
66 LOAD_ATTR 4 (add)
69 LOAD_CONST 2 (1)
72 LOAD_CONST 7 (2)
75 CALL_FUNCTION 2
78 POP_TOP
79 JUMP_FORWARD 0 (to 82)
>> 82 LOAD_CONST 1 (None)
85 RETURN_VALUE
|
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-5-Python-虛擬機 "2.5 Python 虛擬機")2.5 Python 虛擬機
demo.py 被編譯后,接下來的工作就交由 Python 虛擬機來執行字節碼指令了。Python 虛擬機會從編譯得到的 PyCodeObject 對象中依次讀入每一條字節碼指令,并在當前的`上下文環境`中執行這條字節碼指令。我們的程序就是通過這樣循環往復的過程才得以執行。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-6-import-指令 "2.6 import 指令")2.6 import 指令
demo.py 的第一行代碼是?`import foo`。import 指令用來載入一個模塊,另外一個載入模塊的方法是?`from xx import yy`。用 from 語句的好處是,可以只復制需要的符號變量到當前的命名空間中(關于命名空間將在后面介紹)。
前文提到,當已經存在 pyc 文件時,就可以直接載入而省去編譯過程。但是代碼文件的內容會更新,如何保證更新后能重新編譯而不入舊的 pyc 文件呢。答案就在 pyc 文件中存儲的`創建時間信息`。當執行 import 指令的時候,如果已存在 pyc 文件,Python 會檢查創建時間是否晚于代碼文件的修改時間,這樣就能判斷是否需要重新編譯,還是直接載入了。如果不存在 pyc 文件,就會先將 py 文件編譯。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-7-絕對引入和相對引入 "2.7 絕對引入和相對引入")2.7 絕對引入和相對引入
前文已經介紹了?`import foo`?這行代碼。這里隱含了一個問題,就是?`foo`?是什么,如何找到?`foo`。這就屬于 Python 的模塊引入規則,這里不展開介紹,可以參考?[pep-0328](http://legacy.python.org/dev/peps/pep-0328/)。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-8-賦值語句 "2.8 賦值語句")2.8 賦值語句
接下來,執行到?`a = [1, 'python']`,這是一條賦值語句,定義了一個變量 a,它對應的值是 [1, ‘python’]。這里要解釋一下,[變量](http://wilburding.github.io/blog/2013/05/05/what-is-a-variable/)是什么呢?
按照[維基百科](http://www.restran.net/2015/10/22/how-python-code-run/%22https://en.wikipedia.org/wiki/Variable_(computer_science%22)?的解釋
> 變量是一個存儲位置和一個關聯的符號名字,這個存儲位置包含了一些已知或未知的量或者信息。
變量實際上是一個字符串的符號,用來關聯一個存儲在內存中的對象。在 Python 中,會使用 dict(就是 Python 的 dict 對象)來存儲變量符號(字符串)與一個對象的映射。
那么賦值語句實際上就是用來建立這種關聯,在這個例子中是將符號?`a`?與一個列表對象?`[1, 'python']`?建立映射。
緊接著的代碼執行了?`a = 'a string'`,這條指令則將符號?`a`?與另外一個字符串對象?`a string`建立了映射。今后對變量?`a`?的操作,將反應到字符串對象?`a string`?上。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-9-def-指令 "2.9 def 指令")2.9 def 指令
我們的 Python 代碼繼續往下運行,這里執行到一條?`def func()`,從字節碼指令中也可以看出端倪?`MAKE_FUNCTION`。沒錯這條指令是用來創建函數的。Python 是動態語言,def 實際上是執行一條指令,用來創建函數(class 則是創建類的指令),而不僅僅是個語法關鍵字。函數并不是事先創建好的,而是執行到的時候才創建的。
`def func()`?將會創建一個名稱為?`func`?的函數對象。實際上是先創建一個函數對象,然后將 func 這個名稱符號綁定到這個函數上。
> Python 中是無法實現 C 和 Java 中的重載的,因為重載要求函數名要相同,而參數的類型或數量不同,但是 Python 是通過變量符號(如這里的?`func`)來關聯一個函數,當我們用 def 語句再次創建一個同名的函數時,這個變量名就綁定到新的函數對象上了。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-10-動態類型 "2.10 動態類型")2.10 動態類型
繼續看函數?`func`?里面的代碼,這時又有一條賦值語句?`a = 1`。變量?`a`?現在已經變成了第三種類型,它現在是一個整數了。那么 Python 是怎么實現動態類型的呢?答案就藏在具體存儲的對象上。變量?`a`?僅僅只是一個符號(實際上是一個字符串對象),類型信息是存儲在對象上的。在 Python 中,對象機制的核心是類型信息和引用計數(引用計數屬于垃圾回收的部分)。
> 用 type(a),可以輸出 a 的類型,這里是 int
`b = 257`?跳過,我們直接來看看?`print(a + b)`,print 是輸出函數,這里略過。這里想要探究的是?`a + b`。
因為?`a`?和?`b`?并不存儲類型信息,因此當執行?`a + b`?的時候就必須先檢查類型,比如 1 + 2 和 “1” + “2” 的結果是不一樣的。
看到這里,我們就可以想象一下執行一句簡單的?`a + b`,Python 虛擬機需要做多少繁瑣的事情了。首先需要分別檢查?`a`?和?`b`?所對應對象的類型,還要匹配類型是否一致(1 + “2” 將會出現異常),然后根據對象的類型調用正確的?`+`?函數(例如數值的 + 或字符串的 +),~~而 CPU 對于上面這條語句只需要執行 ADD 指令~~(還需要先將變量 MOV 到寄存器)。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-11-命名空間-namespace "2.11 命名空間 (namespace)")2.11 命名空間 (namespace)
在介紹上面的這些代碼時,還漏掉了一個關鍵的信息就是命名空間。在 Python 中,類、函數、module 都對應著一個獨立的命名空間。而一個獨立的命名空間會對應一個 PyCodeObject 對象,所以上面的 demo.py 文件編譯后會生成兩個 PyCodeObject,只是在 demo.py 這個 module 層的 PyCodeObject 中通過一個變量符號?`func`?嵌套了一個函數的 PyCodeObject。
命名空間的意義,就是用來確定一個變量符號到底對應什么對象。命名空間可以一個套一個地形成一條命名空間鏈,Python 虛擬機在執行的過程中,會有很大一部分時間消耗在從這條命名空間鏈中確定一個符號所對應的對象是什么。
在 Python中,命名空間是由一個 dict 對象實現的,它維護了(name,obj)這樣的關聯關系。
說到這里,再補充一下?`import foo`?這行代碼會在 demo.py 這個模塊的命名空間中,創建一個新的變量名?`foo`,`foo`?將綁定到一個 PyCodeObject 對象,也就是 foo.py 的編譯結果。
#### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-11-1-dir-函數 "2.11.1 dir 函數")2.11.1 dir 函數
Python 的[內置函數 dir](https://docs.python.org/2/library/functions.html#dir)?可以用來查看一個命名空間下的所有名字符號。一個用處是查看一個命名空間的所有屬性和方法(這里的命名空間就是指類、函數、module)。
比如,查看當前的命名空間,可以使用 dir(),查看 sys 模塊,可以使用 dir(sys)。
#### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-11-2-LEGB-規則 "2.11.2 LEGB 規則")2.11.2 LEGB 規則
Python 使用 LEGB 的順序來查找一個符號對應的對象
> locals -> enclosing function -> globals -> builtins
**locals**,當前所在命名空間(如函數、模塊),函數的參數也屬于命名空間內的變量
**enclosing**,外部嵌套函數的命名空間(閉包中常見)
|
1
2
3
4
|
def fun1(a):
def fun2():
# a 位于外部嵌套函數的命名空間
print(a)
|
**globals**,全局變量,函數定義所在模塊的命名空間
|
1
2
3
4
5
6
|
a = 1
def fun():
# 需要通過 global 指令來聲明全局變量
global a
# 修改全局變量,而不是創建一個新的 local 變量
a = 2
|
**builtins**,內置模塊的命名空間。Python 在啟動的時候會自動為我們載入很多內置的函數、類,比如 dict,list,type,print,這些都位于?`__builtins__`?模塊中,可以使用?`dir(__builtins__)`來查看。這也是為什么我們在沒有 import 任何模塊的情況下,就能使用這么多豐富的函數和功能了。
介紹完命名空間,就能理解?`print(a)`?這行代碼輸出的結果為什么是?`a string`?了。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-12-內置屬性-name "2.12 內置屬性 __name__")2.12 內置屬性?`__name__`
現在到了解釋?`if __name__ == '__main__'`?這行代碼的時候了。當 Python 程序啟動后,Python 會自動為每個模塊設置一個屬性?`__name__`?通常使用的是模塊的名字,也就是文件名,但唯一的例外是主模塊,主模塊將會被設置為?`__main__`。利用這一特性,就可以做一些特別的事。比如當該模塊以主模塊來運行的時候,可以運行測試用例。而當被其他模塊 import 時,則只是乖乖的,提供函數和功能就好。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#2-13-函數調用 "2.13 函數調用")2.13 函數調用
最后兩行是函數調用,這里略去不講。
## [](http://www.restran.net/2015/10/22/how-python-code-run/#3-回顧 "3\. 回顧")3\. 回顧
講到最后,還有些內容需要再回顧和補充一下。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-1-pyc-文件 "3.1 pyc 文件")3.1 pyc 文件
Python 只會對那些以后可能繼續被使用和載入的模塊才會生成 pyc 文件,Python 認為使用了 import 指令的模塊,屬于這種類型,因此會生成 pyc 文件。而對于只是臨時用一次的模塊,并不會生成 pyc 文件,Python 將主模塊當成了這種類型的文件。這就解釋了為什么?`python demo.py`?執行完后,只會生成一個?`foo.pyc`?文件。
> 如果要問 pyc 文件什么時候生成,答案就是在執行了 import 指令之后,from xx import yy 同樣屬于 import 指令。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-2-小整數對象池 "3.2 小整數對象池")3.2 小整數對象池
在 demo.py 這里例子中,所用的整數特意用了一個 257,這是為了介紹小整數對象池的。整數在程序中的使用非常廣泛,Python 為了優化速度,使用了小整數對象池,避免為整數頻繁申請和銷毀內存空間。
Python 對小整數的定義是 [-5, 257),這些整數對象是提前建立好的,不會被垃圾回收。在一個 Python 的程序中,所有位于這個范圍內的整數使用的都是同一個對象,從下面這個例子就可以看出。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> a = 1
>>> id(a)
40059744
>>> b = 1
>>> id(b)
40059744
>>> c = 257
>>> id(c)
41069072
>>> d = 257
>>> id(257)
41069096
|
> id 函數可以用來查看一個對象的唯一標志,可以認為是內存地址
對于大整數,Python 使用的是一個`大整數對象池`。這句話的意思是:
> 每當創建一個大整數的時候,都會新建一個對象,但是這個對象不再使用的時候,并不會銷毀,后面再建立的對象會復用之前已經不再使用的對象的內存空間。(這里的不再使用指的是引用計數為0,可以被銷毀)
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-3-字符串對象緩沖池 "3.3 字符串對象緩沖池")3.3 字符串對象緩沖池
如果仔細思考一下,一定會猜到字符串也采用了這種類似的技術,我們來看一下
|
1
2
3
4
5
6
|
>>> a = 'a'
>>> b = 'a'
>>> id(a)
14660456
>>> id(b)
14660456
|
沒錯,Python 的設計者為`一個字節`的字符對應的字符串對象 (PyStringObject) 也設計了這樣一個對象池。同時還有一個?`intern`?機制,可以將內容相同的字符串變量轉換成指向同一個字符串對象。
intern 機制的關鍵,就是在系統中有一個(key,value)映射關系的集合,集合的名稱叫做 interned。在這個集合中,記錄著被 intern 機制處理過的 PyStringObject 對象。不過 Python 始終會為字符串創建 PyStringObject 對象,即便在interned 中已經有一個與之對應的 PyStringObject 對象了,而 intern 機制是在字符串被創建后才起作用。
|
1
2
3
4
5
6
7
8
|
>>> a = 'a string'
>>> b = 'a string'
>>> a is b
False
>>> a = intern('a string') # 手動調用 intern 方法
>>> b = intern('a string')
>>> a is b
True
|
關于 intern 函數 可以參考[官方文檔](https://docs.python.org/2/library/functions.html#intern),更多擴展閱讀:
[http://stackoverflow.com/questions/15541404/python-string-interning](http://stackoverflow.com/questions/15541404/python-string-interning)
> 值得說明的是,數值類型和字符串類型在 Python 中都是不可變的,這意味著你無法修改這個對象的值,每次對變量的修改,實際上是創建一個新的對象。得益于這樣的設計,才能使用對象緩沖池這種優化。
Python 的實現上大量采用了這種內存對象池的技術,不僅僅對于這些特定的對象,還有專門的內存池用于小對象,使用這種技術可以避免頻繁地申請和釋放內存空間,目的就是讓 Python 能稍微更快一點。更多內容可以參考[這里](http://blog.csdn.net/zhzhl202/article/details/7547445)。
> 如果想了解更快的 Python,可以看看?[PyPy](http://pypy.org/)
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-4-import-指令 "3.4 import 指令")3.4 import 指令
前文提到 import 指令是用來載入 module 的,如果需要,也會順道做編譯的事。但 import 指令,還會做一件重要的事情就是把 import 的那個 module 的代碼執行一遍,`這件事情很重要`。Python 是解釋執行的,連函數都是執行的時候才創建的。如果不把那個 module 的代碼執行一遍,那么 module 里面的函數都沒法創建,更別提去調用這些函數了。
執行代碼的另外一個重要作用,就是在這個 module 的命名空間中,創建模塊內定義的函數和各種對象的符號名稱(也就是變量名),并將其綁定到對象上,這樣其他 module 才能通過變量名來引用這些對象。
Python 虛擬機還會將已經 import 過的 module 緩存起來,放到一個全局 module 集合 sys.modules 中。這樣做有一個好處,即如果程序的在另一個地方再次 import 這個模塊,Python 虛擬機只需要將全局 module 集合中緩存的那個 module 對象返回即可。
> 你現在一定想到了 sys.modules 是一個 dict 對象,可以通過 type(sys.modules) 來驗證
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-5-多線程 "3.5 多線程")3.5 多線程
demo.py 這個例子并沒有用到多線程,但還是有必要提一下。
在提到多線程的時候,往往要關注線程如何同步,如何訪問共享資源。Python 是通過一個全局解釋器鎖 GIL(Global Interpreter Lock)來實現線程同步的。當 Python 程序只有單線程時,并不會啟用 GIL,而當用戶創建了一個 thread 時,表示要使用多線程,Python 解釋器就會自動激活 GIL,并創建所需要的上下文環境和數據結構。
Python 字節碼解釋器的工作原理是按照指令的順序一條一條地順序執行,Python 內部維護著一個數值,這個數值就是 Python 內部的時鐘,如果這個數值為 N,則意味著 Python 在執行了 N 條指令以后應該立即啟動線程調度機制,可以通過下面的代碼獲取這個數值。
|
1
2
|
import sys
sys.getcheckinterval() # 100
|
線程調度機制將會為線程分配 GIL,獲取到 GIL 的線程就能開始執行,而其他線程則必須等待。由于 GIL 的存在,Python 的多線程性能十分低下,無法發揮多核 CPU 的優勢,性能甚至不如單線程。因此如果你想用到多核 CPU,一個建議是使用`多進程`。
### [](http://www.restran.net/2015/10/22/how-python-code-run/#3-6-垃圾回收 "3.6 垃圾回收")3.6 垃圾回收
在講到垃圾回收的時候,通常會使用引用計數的模型,這是一種最直觀,最簡單的垃圾收集技術。Python 同樣也使用了引用計數,但是引用計數存在這些缺點:
* 頻繁更新引用計數會降低運行效率
* 引用計數無法解決循環引用問題
Python 在`引用計數機制`的基礎上,使用了主流垃圾收集技術中的`標記——清除`和`分代收集`兩種技術。
關于垃圾回收,可以參考
[http://hbprotoss.github.io/posts/pythonla-ji-hui-shou-ji-zhi.html](http://hbprotoss.github.io/posts/pythonla-ji-hui-shou-ji-zhi.html)
## [](http://www.restran.net/2015/10/22/how-python-code-run/#4-參考文獻 "4\. 參考文獻")4\. 參考文獻
* [Python 源碼剖析](http://book.douban.com/subject/3117898/)
* [Python 官方文檔](https://docs.python.org/2/library/index.html)
- 空白目錄
- serial
- serial 整理
- hex to str
- Python3 字符編碼
- 字符串編碼與Python 3編碼
- python3中bytes和string之間的互相轉換
- Python3 字符編碼
- python整數、字符串、字節串相互轉換
- python整數、字符串、字節串相互轉換
- python常用的十進制、16進制、字符串、字節串之間的轉換(長期更新帖)
- python中pyserial模塊使用方法
- 談談 Python 程序的運行原理
- flask
- Flask 在 Debug 模式下初始化2次
- Flask中向前端傳遞或者接收Json文件的方法
- 使用 Python 和 Flask 設計 RESTful API
- 使用 Flask-RESTful 設計 RESTful API