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