Python 是一種半編譯半解釋型運行環(huán)境。首先,它會在模塊 "載入" 時將源碼編譯成字節(jié)碼 (Byte Code)。而后,這些字節(jié)碼會被虛擬機在一個 "巨大" 的核心函數(shù)里解釋執(zhí)行。這是導致 Python 性能較低的重要原因,好在現(xiàn)在有了內置 Just-in-time 二次編譯器的 PyPy 可供選擇。
當虛擬機開始運行時,它通過初始化函數(shù)完成整個運行環(huán)境設置:
Python 源碼是個寶庫,其中有大量的編程范式和技巧可供借鑒,尤其是對內存的管理分配。個人建議有 C 基礎的兄弟,在閑暇時翻看一二。
先有類型 (Type),而后才能生成實例 (Instance)。Python 中的一切都是對象,包括類型在內的每個對象都包含一個標準頭,通過頭部信息就可以明確知道其具體類型。
頭信息由 "引用計數(shù)" 和 "類型指針" 組成,前者在對象被引用時增加,超出作用域或手工釋放后減小,等于 0 時會被虛擬機回收 (某些被緩存的對象計數(shù)器永遠不會為 0)。
以 int 為例,對應 Python 結構定義是:
#define PyObject_HEAD \
Py_ssize_t ob_refcnt; \
struct _typeobject *ob_type;
typedef struct _object {
PyObject_HEAD
} PyObject;
typedef struct {
PyObject_HEAD // 在 64 位版本中,頭長度為 16 字節(jié)。
long ob_ival; // long 是 8 字節(jié)。
} PyIntObject;
可以用 sys 中的函數(shù)測試一下。
>>> import sys
>>> x = 0x1234 # 不要使用 [-5, 257) 之間的小數(shù)字,它們有專門的緩存機制。
>>> sys.getsizeof(x) # 符合長度預期。
24
>>> sys.getrefcount(x) # sys.getrefcount() 讀取頭部引用計數(shù),注意形參也會增加一次引用。
2
>>> y = x # 引用計數(shù)增加。
>>> sys.getrefcount(x)
3
>>> del y # 引用計數(shù)減小。
>>> sys.getrefcount(x)
2
類型指針則指向具體的類型對象,其中包含了繼承關系、靜態(tài)成員等信息。所有的內置類型對象都能從 types 模塊中找到,至于 int、long、str 這些關鍵字可以看做是簡短別名。
>>> import types
>>> x = 20
>>> type(x) is types.IntType # is 通過指針判斷是否指向同一對象。
True
>>> x.__class__ # __class__ 通過類型指針來獲取類型對象。
<type 'int'>
>>> x.__class__ is type(x) is int is types.IntType
True
>>> y = x
>>> hex(id(x)), hex(id(y)) # id() 返回對象標識,其實就是內存地址。
('0x7fc5204103c0', '0x7fc5204103c0')
>>> hex(id(int)), hex(id(types.IntType))
('0x1088cebd8', '0x1088cebd8')
除了 int 這樣的固定長度類型外,還有 long、str 這類變長對象。其頭部多出一個記錄元素項數(shù)量的字段。比如 str 的字節(jié)數(shù)量,list 列表的長度等等。
#define PyObject_VAR_HEAD \
PyObject_HEAD \
Py_ssize_t ob_size; /* Number of items in variable part */
typedef struct {
PyObject_VAR_HEAD
} PyVarObject;
有關類型和對象更多的信息,將在后續(xù)章節(jié)中詳述。
名字空間是 Python 最核心的內容。
>>> x
NameError: name 'x' is not defined
我們習慣于將 x 稱為變量,但在這里,更準確的詞語是 "名字"。
和 C 變量名是內存地址別名不同,Python 的名字實際上是一個字符串對象,它和所指向的目標對象一起在名字空間中構成一項 {name: object} 關聯(lián)。
Python 有多種名字空間,比如稱為 globals 的模塊名字空間,稱為 locals 的函數(shù)堆棧幀名字空間,還有 class、instance 名字空間。不同的名字空間決定了對象的作用域和生存周期。
>>> x = 123
>>> globals() # 獲取 module 名字空間。
{'x': 123, ......}
可以看出,名字空間就是一個字典 (dict)。我們完全可以直接在名字空間添加項來創(chuàng)建名字。
>>> globals()["y"] = "Hello, World"
>>> y
'Hello, World'
在 Python 源碼中,有這樣一句話:Names have no type, but objects do.
名字的作用僅僅是在某個時刻與名字空間中的某個對象進行關聯(lián)。其本身不包含目標對象的任何信息,只有通過對象頭部的類型指針才能獲知其具體類型,進而查找其相關成員數(shù)據(jù)。正因為名字的弱類型特征,我們可以在運行期隨時將其關聯(lián)到任何類型對象。
>>> y
'Hello, World'
>>> type(y)
<type 'str'>
>>> y = __import__("string") # 將原本與字符串關聯(lián)的名字指向模塊對象。
>>> type(y)
<type 'module'>
>>> y.digits # 查看模塊對象的成員。
'0123456789'
在函數(shù)外部,locals() 和 globals() 作用完全相同。而當在函數(shù)內部調用時,locals() 則是獲取當前函數(shù)堆棧幀的名字空間,其中存儲的是函數(shù)參數(shù)、局部變量等信息。
>>> import sys
>>> globals() is locals()
True
>>> locals()
{
'__builtins__': <module '__builtin__' (built-in)>,
'__name__': '__main__',
'sys': <module 'sys' (built-in)>,
}
>>> def test(x): # 請對比下面的輸出內容。
... y = x + 100
... print locals() # 可以看到 locals 名字空間中包含當前局部變量。
... print globals() is locals() # 此時 locals 和 globals 指向不同名字空間。
... frame = sys._getframe(0) # _getframe(0) 獲取當前堆棧幀。
... print locals() is frame.f_locals # locals 名字空間實際就是當前堆棧幀的名字空間。
... print globals() is frame.f_globals # 通過 frame 我們也可以函數(shù)定義模塊的名字空間。
>>> test(123)
{'y': 223, 'x': 123}
False
True
True
在函數(shù)中調用 globals() 時,總是獲取包含該函數(shù)定義的模塊名字空間,而非調用處。
>>> pycat test.py
a = 1
def test():
print {k:v for k, v in globals().items() if k = "__builtins__"}
>>> import test
>>> test.test()
{
'__file__': 'test.pyc',
'__name__': 'test',
'a': 1,
'test': <function test at 0x10bd85e60>,
}
可通過
>>> test.__dict__ # test 模塊的名字空間
{
'__file__': 'test.pyc',
'__name__': 'test',
'a': 1,
'test': <function test at 0x10bd85e60>,
}
>>> import sys
>>> sys.modules[__name__].__dict__ is globals() # 當前模塊名字空間和 globals 相同。
True
與名字空間有關的內容很多,比如作用域、LEGB 查找規(guī)則、成員查找規(guī)則等等。所有這些,都將在相關章節(jié)中給出詳細說明。
使用名字空間管理上下文對象,帶來無與倫比的靈活性,但也犧牲了執(zhí)行性能。畢竟從字典中查找對象遠比指針低效很多,各有得失。
為提升執(zhí)行性能,Python 在內存管理上做了大量工作。最直接的做法就是用內存池來減少操作系統(tǒng)內存分配和回收操作,那些小于等于 256 字節(jié)對象,將直接從內存池中獲取存儲空間。
根據(jù)需要,虛擬機每次從操作系統(tǒng)申請一塊 256KB,取名為 arena 的大塊內存。并按系統(tǒng)頁大小,劃分成多個 pool。每個 pool 繼續(xù)分割成 n 個大小相同的 block,這是內存池最小存儲單位。
block 大小是 8 的倍數(shù),也就是說存儲 13 字節(jié)大小的對象,需要找 block 大小為 16 的 pool 獲取空閑塊。所有這些都用頭信息和鏈表管理起來,以便快速查找空閑區(qū)域進行分配。
大于 256 字節(jié)的對象,直接用 malloc 在堆上分配內存。程序運行中的絕大多數(shù)對象都小于這個閾值,因此內存池策略可有效提升性能。
當所有 arena 的總容量超出限制 (64MB) 時,就不再請求新的 arena 內存。而是如同 "大對象" 一樣,直接在堆上為對象分配內存。另外,完全空閑的 arena 會被釋放,其內存交還給操作系統(tǒng)。
引用傳遞
對象總是按引用傳遞,簡單點說就是通過復制指針來實現(xiàn)多個名字指向同一對象。因為 arena 也是在堆上分配的,所以無論何種類型何種大小的對象,都存儲在堆上。Python 沒有值類型和引用類型一說,就算是最簡單的整數(shù)也是擁有標準頭的完整對象。
>>> a = object()
>>> b = a
>>> a is b
True
>>> hex(id(a)), hex(id(b)) # 地址相同,意味著對象是同一個。
('0x10b1f5640', '0x10b1f5640')
>>> def test(x):
... print hex(id(x))
>>> test(a)
0x10b1f5640 # 地址依舊相同。
如果不希望對象被修改,就需使用不可變類型,或對象復制品。
不可變類型:int, long, str, tuple, frozenset
除了某些類型自帶的 copy 方法外,還可以:
下面的測試建議不要用數(shù)字等不可變對象,因為其內部的緩存和復用機制可能會造成干擾。
>>> import copy
>>> x = object()
>>> l = [x] # 創(chuàng)建一個列表。
>>> l2 = copy.copy(l) # 淺復制,僅復制對象自身,而不會遞歸復制其成員。
>>> l2 is l # 可以看到復制列表的元素依然是原對象。
False
>>> l2[0] is x
True
>>> l3 = copy.deepcopy(l) # 深度復制,會遞歸復制所有深度成員。
>>> l3 is l # 列表元素也被復制了。
False
>>> l3[0] is x
False
循環(huán)引用會影響 deepcopy 函數(shù)的運作,建議查閱官方標準庫文檔。
引用計數(shù)
Python 默認采用引用計數(shù)來管理對象的內存回收。當引用計數(shù)為 0 時,將立即回收該對象內存,要么將對應的 block 塊標記為空閑,要么返還給操作系統(tǒng)。
為觀察回收行為,我們用 del 監(jiān)控對象釋放。
>>> class User(object):
... def __del__(self):
... print "Will be dead"
>>> a = User()
>>> b = a
>>> import sys
>>> sys.getrefcount(a)
3
>>> del a # 刪除引用,計數(shù)減小。
>>> sys.getrefcount(b)
2
>>> del b # 刪除最后一個引用,計數(shù)器為 0,對象被回收。
Will be dead
某些內置類型,比如小整數(shù),因為緩存的緣故,計數(shù)永遠不會為 0,直到進程結束才由虛擬機清理函數(shù)釋放。
除了直接引用外,Python 還支持弱引用。允許在不增加引用計數(shù),不妨礙對象回收的情況下間接引用對象。但不是所有類型都支持弱引用,比如 list、dict ,弱引用會引發(fā)異常。
改用弱引用回調監(jiān)控對象回收。
>>> import sys, weakref
>>> class User(object): pass
>>> def callback(r): # 回調函數(shù)會在原對象被回收時調用。
... print "weakref object:", r
... print "target object dead"
>>> a = User()
>>> r = weakref.ref(a, callback) # 創(chuàng)建弱引用對象。
>>> sys.getrefcount(a) # 可以看到弱引用沒有導致目標對象引用計數(shù)增加。
2 # 計數(shù) 2 是因為 getrefcount 形參造成的。
>>> r() is a # 透過弱引用可以訪問原對象。
True
>>> del a # 原對象回收,callback 被調用。
weakref object: <weakref at 0x10f99a368; dead>
target object dead
>>> hex(id(r)) # 通過對比,可以看到 callback 參數(shù)是弱引用對象。
'0x10f99a368' # 因為原對象已經(jīng)死亡。
>>> r() is None # 此時弱引用只能返回 None。也可以此判斷原對象死亡。
True
引用計數(shù)是一種簡單直接,并且十分高效的內存回收方式。大多數(shù)時候它都能很好地工作,除了循環(huán)引用造成計數(shù)故障。簡單明顯的循環(huán)引用,可以用弱引用打破循環(huán)關系。但在實際開發(fā)中,循環(huán)引用的形成往往很復雜,可能由 n 個對象間接形成一個大的循環(huán)體,此時只有靠 GC 去回收了。
垃圾回收
事實上,Python 擁有兩套垃圾回收機制。除了引用計數(shù),還有個專門處理循環(huán)引用的 GC。通常我們提到垃圾回收時,都是指這個 "Reference Cycle Garbage Collection"。
能引發(fā)循環(huán)引用問題的,都是那種容器類對象,比如 list、set、object 等。對于這類對象,虛擬機在為其分配內存時,會額外添加用于追蹤的 PyGC_Head。這些對象被添加到特殊鏈表里,以便 GC 進行管理。
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy;
} PyGC_Head;
當然,這并不表示此類對象非得 GC 才能回收。如果不存在循環(huán)引用,自然是積極性更高的引用計數(shù)機制搶先給處理掉。也就是說,只要不存在循環(huán)引用,理論上可以禁用 GC。當執(zhí)行某些密集運算時,臨時關掉 GC 有助于提升性能。
>>> import gc
>>> class User(object):
... def __del__(self):
... print hex(id(self)), "will be dead"
>>> gc.disable() # 關掉 GC
>>> a = User()
>>> del a # 對象正常回收,引用計數(shù)不會依賴 GC。
0x10fddf590 will be dead
同 .NET、JAVA 一樣,Python GC 同樣將要回收的對象分成 3 級代齡。GEN0 管理新近加入的年輕對象,GEN1 則是在上次回收后依然存活的對象,剩下 GEN2 存儲的都是生命周期極長的家伙。每級代齡都有一個最大容量閾值,每次 GEN0 對象數(shù)量超出閾值時,都將引發(fā)垃圾回收操作。
#define NUM_GENERATIONS 3
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0},
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0},
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0},
};
GC 首先檢查 GEN2,如閾值被突破,那么合并 GEN2、GEN1、GEN0 幾個追蹤鏈表。如果沒有超出,則檢查 GEN1。GC 將存活的對象提升代齡,而那些可回收對象則被打破循環(huán)引用,放到專門的列表等待回收。
>>> gc.get_threshold() # 獲取各級代齡閾值
(700, 10, 10)
>>> gc.get_count() # 各級代齡鏈表跟蹤的對象數(shù)量
(203, 0, 5)
包含 del 方法的循環(huán)引用對象,永遠不會被 GC 回收,直至進程終止。
這回不能偷懶用 del 監(jiān)控對象回收了,改用 weakref。因 IPython 對 GC 存在干擾,下面的測試代碼建議在原生 shell 中進行。
>>> import gc, weakref
>>> class User(object): pass
>>> def callback(r): print r, "dead"
>>> gc.disable() # 停掉 GC,看看引用計數(shù)的能力。
>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a # 形成循環(huán)引用關系。
>>> del a; del b # 刪除名字引用。
>>> wa(), wb() # 顯然,計數(shù)機制對循環(huán)引用無效。
(<__main__.User object at 0x1045f4f50>, <__main__.User object at 0x1045f4f90>)
>>> gc.enable() # 開啟 GC。
>>> gc.isenabled() # 可以用 isenabled 確認。
True
>>> gc.collect() # 因為沒有達到閾值,我們手工啟動回收。
<weakref at 0x1045a8cb0; dead> dead # GC 的確有對付基友的能力。
<weakref at 0x1045a8db8; dead> dead # 這個地址是弱引用對象的,別犯糊涂。
一旦有了 del,GC 就拿循環(huán)引用沒辦法了。
>>> import gc, weakref
>>> class User(object):
... def __del__(self): pass # 難道連空的 __del__ 也不行?
>>> def callback(r): print r, "dead"
>>> gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # 輸出更詳細的回收狀態(tài)信息。
>>> gc.isenabled() # 確保 GC 在工作。
True
>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a
>>> del a; del b
>>> gc.collect() # 從輸出信息看,回收失敗。
gc: collecting generation 2...
gc: objects in each generation: 520 3190 0
gc: uncollectable <User 0x10fd51fd0> # a
gc: uncollectable <User 0x10fd57050> # b
gc: uncollectable <dict 0x7f990ac88280> # a.__dict__
gc: uncollectable <dict 0x7f990ac88940> # b.__dict__
gc: done, 4 unreachable, 4 uncollectable, 0.0014s elapsed.
4
>>> xa = wa()
>>> xa, hex(id(xa.__dict__))
<__main__.User object at 0x10fd51fd0>, '0x7f990ac88280',
>>> xb = wb()
>>> xb, hex(id(xb.__dict__))
<__main__.User object at 0x10fd57050>, '0x7f990ac88940'
關于用不用 del 的爭論很多。大多數(shù)人的結論是堅決抵制,諸多 "牛人" 也是這樣教導新手的。可畢竟 del 承擔了析構函數(shù)的角色,某些時候還是有其特定的作用的。用弱引用回調會造成邏輯分離,不便于維護。對于一些簡單的腳本,我們還是能保證避免循環(huán)引用的,那不妨試試。就像前面例子中用來監(jiān)測對象回收,就很方便。
Python 實現(xiàn)了棧式虛擬機 (Stack-Based VM) 架構,通過與機器無關的字節(jié)碼來實現(xiàn)跨平臺執(zhí)行能力。這種字節(jié)碼指令集沒有寄存器,完全以棧 (抽象層面) 進行指令運算。盡管很簡單,但對普通開發(fā)人員而言,是無需關心的細節(jié)。
要運行 Python 語言編寫的程序,必須將源碼編譯成字節(jié)碼。通常情況下,編譯器會將源碼轉換成字節(jié)碼后保存在 pyc 文件中。還可用 -O 參數(shù)生成 pyo 格式,這是簡單優(yōu)化后的 pyc 文件。
編譯發(fā)生在模塊載入那一刻。具體來看,又分為 pyc 和 py 兩種情況。
載入 pyc 流程:
如果沒有 pyc,那么就需要先完成編譯:
Magic 是一個特殊的數(shù)字,由 Python 版本號計算得來,作為 pyc 文件和 Python 版本檢查標記。PyCodeObject 則包含了代碼對象的完整信息。
typedef struct {
PyObject_HEAD
int co_argcount; // 參數(shù)個數(shù),不包括 *args, **kwargs。
int co_nlocals; // 局部變量數(shù)量。
int co_stacksize; // 執(zhí)行所需的??臻g。
int co_flags; // 編譯標志,在創(chuàng)建 Frame 時用得著。
PyObject *co_code; // 字節(jié)碼指令。
PyObject *co_consts; // 常量列表。
PyObject *co_names; // 符號列表。
PyObject *co_varnames; // 局部變量名列表。
PyObject *co_freevars; // 閉包: 引用外部函數(shù)名字列表。
PyObject *co_cellvars; // 閉包: 被內部函數(shù)引用的名字列表。
PyObject *co_filename; // 源碼文件名。
PyObject *co_name; // PyCodeObject 的名字,函數(shù)名、類名什么的。
int co_firstlineno; // 這個 PyCodeObject 在源碼文件中的起始位置,也就是行號。
PyObject *co_lnotab; // 字節(jié)碼指令偏移量和源碼行號的對應關系,反匯編時用得著。
void *co_zombieframe; // 為優(yōu)化準備的特殊 Frame 對象。
PyObject *co_weakreflist; // 為弱引用準備的...
} PyCodeObject;
無論是模塊還是其內部的函數(shù),都被編譯成 PyCodeObject 對象。內部成員都嵌套到 co_consts 列表中。
>>> pycat test.py
"""
Hello, World
"""
def add(a, b):
return a + b
c = add(10, 20)
>>> code = compile(open("test.py").read(), "test.py", "exec")
>>> code.co_filename, code.co_name, code.co_names
('test.py', '<module>', ('__doc__', 'add', 'c'))
>>> code.co_consts
('\n Hello, World\n', <code object add at 0x105b76e30, file "test.py", line 5>, 10,
20, None)
>>> add = code.co_consts[1]
>>> add.co_varnames
('a', 'b')
除了內置 compile 函數(shù),標準庫里還有 py_compile、compileall 可供選擇。
>>> import py_compile, compileall
>>> py_compile.compile("test.py", "test.pyo")
>>> ls
main.py* test.py test.pyo
>>> compileall.compile_dir(".", 0)
Listing . ...
Compiling ./main.py ...
Compiling ./test.py ...
如果對 pyc 文件格式有興趣,但又不想看 C 代碼,可以到 /usr/lib/python2.7/compiler 目錄里尋寶。又或者你對反匯編、代碼混淆、代碼注入等話題更有興趣,不妨看看標準庫里的 dis。
相比 .NET、JAVA 的 CodeDOM 和 Emit,Python 天生擁有無與倫比的動態(tài)執(zhí)行優(yōu)勢。
最簡單的就是用 eval() 執(zhí)行表達式。
>>> eval("(1 + 2) * 3") # 假裝看不懂這是啥……
9
>>> eval("{'a': 1, 'b': 2}") # 將字符串轉換為 dict。
{'a': 1, 'b': 2}
eval 默認會使用當前環(huán)境的名字空間,當然我們也可以帶入自定義字典。
>>> x = 100
>>> eval("x + 200") # 使用當前上下文的名字空間。
300
>>> ns = dict(x = 10, y = 20)
>>> eval("x + y", ns) # 使用自定義名字空間。
30
>>> ns.keys() # 名字空間里多了 __builtins__。
['y', 'x', '__builtins__']
要執(zhí)行代碼片段,或者 PyCodeObject 對象,那么就需要動用 exec 。同樣可以帶入自定義名字空間,以避免對當前環(huán)境造成污染。
>>> py = """
... class User(object):
... def __init__(self, name):
... self.name = name
... def __repr__(self):
... return "<User: {0:x}; name={1}>".format(id(self), self.name)
... """
>>> ns = dict()
>>> exec py in ns # 執(zhí)行代碼片段,使用自定義的名字空間。
>>> ns.keys() # 可以看到名字空間包含了新的類型:User。
['__builtins__', 'User']
>>> ns["User"]("Tom") # 完全可用。貌似用來開發(fā) ORM 會很簡單。
<User: 10547f290; name=Tom>
繼續(xù)看 exec 執(zhí)行 PyCodeObject 的演示。
>>> py = """
... def incr(x):
... global z
... z += x
... """
>>> code = compile(py, "test", "exec") # 編譯成 PyCodeObject。
>>> ns = dict(z = 100) # 自定義名字空間。
>>> exec code in ns # exec 執(zhí)行以后,名字空間多了 incr。
>>> ns.keys() # def 的意思是創(chuàng)建一個函數(shù)對象。
['__builtins__', 'incr', 'z']
>>> exec "incr(x); print z" in ns, dict(x = 50) # 試著調用這個 incr,不過這次我們提供一個
150 # local 名字空間,以免污染 global。
>>> ns.keys() # 污染沒有發(fā)生。
['__builtins__', 'incr', 'z']
動態(tài)執(zhí)行一個 py 文件,可以考慮用 execfile(),或者 runpy 模塊。