Python拾遺(1)
讀《Python
原始碼分析》的一些記錄。
虛擬機器
python
會在模組載入時將原始碼編譯成位元組碼,然後位元組碼會被虛擬機器在核心函式中解釋執行。
虛擬機器開始執行時,通過初始化函式完成整個執行環境設定,包括:
- 建立
__builtlin__
模組,持有所有內建型別和函式 - 建立
sys
模組 - 初始化內建
Exception
- 建立
__main__
模組,準備所需名字空間 - 通過
site.py
將site-packages
中的第三方擴充套件庫新增到搜尋路徑列表 - 執行入口
py
檔案,執行前將__main__.__dict__
作為名字空間傳遞進去 - 程式執行結束後執行清理操作
型別和物件
每個物件都包含一個標準頭,通過頭部資訊就可以明確知道其具體型別。頭部資訊由“引用計數”和“型別指標”組成。
>>> import sys
>>> x = 0x1234
>>> sys.getsizeof(x)
24
>>> sys.getrefcount(x) //作為形參也會增加一次引用計數
2
>>> y = x
>>> sys.getrefcount(x)
3
>>> del y
>>> sys.getrefcount(x)
2
所有內建型別物件都能從types
模組中找到,int
、long
、str
都可以看做是簡短別名:
>>> import types >>> x = 20 >>> type(x) is types.IntType True >>> x.__class__ <type 'int'> >>> x.__class__ is type(x) True >>> x.__class__ is int True >>> x.__class__ is types.IntType True
名稱空間
實際上python
中,名字實際上是字串物件,和所指向的目標物件在名稱空間中構成{name: object}
關聯。它有多種名稱空間,比如globals
(模組名稱空間),locals
函式堆疊幀名稱空間,以及class
、instance
等。
>>> globals() {'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'sys': <module 'sys' (built-in)>, 'x': 123, '__name__': '__main__', '__doc__': None, 'types': <module 'types' from '/usr/lib/python2.7/types.pyc'>} >>> globals()["y"] = "hello" >>> y 'hello'
名字的作用僅僅是在某個時刻與名字空間的某個物件進行關聯。其本身不包含目標物件的任何資訊,只有通過物件頭部的型別指標才能獲得其具體型別。因此可以在執行期隨時將其關聯到任何型別物件。
在函式外部時,locals()
和globals()
作用完全相同,在函式內部呼叫時,locals()
則是獲取當前函式堆疊幀的名稱空間,其中儲存的是函式引數、區域性變數等。
>>> import sys
>>> def test(x):
... y = x + 100
... print locals()
... print globals() is locals()
... frame = sys._getframe(0) # 獲取當前堆疊幀
... print locals() is frame.f_locals
... print globals() is frame.f_globals
...
>>> test(123)
{'y': 223, 'x': 123}
False
True
True
在函式內部中呼叫globals()
時,總是獲取包含該函式定義的模組名稱空間,而非呼叫處的:
# test.py
# -*- coding: utf-8 -*-
a = 1
def test():
print {k:v for k, v in globals().items() if k != "__builtins__"}
# test1.py
import test
print test.test()
# 結果
{'a': 1, '__file__': '/home/may/study/test.py', '__package__': None, 'test': <function test at 0x7f6fc1119848>, '__name__': 'test', '__doc__': None}
可以通過<module>.__dict__
訪問其他模組的名稱空間。
>>> import sys
>>> sys.modules[__name__].__dict__ is globals() //當前模組名稱空間和globals相同
True
然而使用名稱空間管理上下文,雖然能帶來靈活性,但是相比指標低效很多,因此犧牲了一部分的執行效能。
記憶體管理
python
採用記憶體池減少作業系統記憶體分配和回收操作,小於等於256
位元組物件,將直接從記憶體池中獲取儲存空間。根據需要,虛擬機器每次從作業系統申請一塊256kb
的記憶體(areana
),並且根據系統頁劃分為多個pool
。每個pool
分割成以8
為倍數的block
,這是記憶體池的最小單位。
大於256
位元組的物件,直接使用malloc
在堆上分配記憶體。
當areana
總容量超出64MB
時,不再請求新的arena
,而是直接在堆上為物件分配記憶體。完全空閒的arena
會被釋放交還給作業系統。
引用傳遞
物件總是按引用傳遞,即通過複製指標來實現多個名字指向同一物件。因為arena
是在堆上分配的,所以無論何種型別何種大小的物件,都儲存在堆上。因此,沒有值型別和引用型別的區分。
不可變型別:
int, long, str, tuple, frozenset
深拷貝與淺拷貝
淺拷貝是對引用的拷貝(僅複製物件自身,而不會遞迴複製其成員),深拷貝是對物件資源的拷貝。
需要注意的是:
- 不可變型別當修改時會建立新的物件,產生新的地址
- 切片操作
[:]
,使用工廠函式list/dict/set
以及copy()
都會產生淺拷貝的效果
賦值操作:
In [57]: will = ["will", 28, ["python", "c#", "javascript"]]
In [58]: wilber = will
In [59]: wilber is will
Out[59]: True
In [60]: print(id(will),id(wilber))
140110220425032 140110220425032
In [61]: print([id(element) for element in will])
[140110445106880, 10912032, 140110220811400]
In [62]: print([id(element) for element in wilber])
[140110445106880, 10912032, 140110220811400]
In [63]: will[0] = 'wilber'
In [64]: print(id(will),id(wilber))
140110220425032 140110220425032
In [65]: print([id(element) for element in will])
[140110221716032, 10912032, 140110220811400]
In [66]: print([id(element) for element in wilber])
[140110221716032, 10912032, 140110220811400]
賦值操作相當於將兩個指標都指向了同一個地址
淺拷貝:
In [67]: import copy
In [68]: wilber = copy.copy(will)
# 淺拷貝會建立一個新的物件
In [69]: print(id(will),id(wilber))
140110220425032 140110470159432
# 對於物件中的元素,只會使用原始元素的記憶體地址
In [70]: print([id(element) for element in will])
[140110221716032, 10912032, 140110220811400]
In [71]: print([id(element) for element in wilber])
[140110221716032, 10912032, 140110220811400]
In [72]: will[0] = 'will'
In [73]: will[2].append('css')
# 對不可變物件的修改會建立新的物件,對可變物件的修改會影響新的物件
In [74]: will, wilber
Out[74]:
(['will', 28, ['python', 'c#', 'javascript', 'css']],
['wilber', 28, ['python', 'c#', 'javascript', 'css']])
In [75]: print([id(element) for element in will])
[140110445106880, 10912032, 140110220811400]
In [76]: print([id(element) for element in wilber])
[140110221716032, 10912032, 140110220811400]
深拷貝:
In [77]: wilber = copy.deepcopy(will)
In [78]: print(id(will),id(wilber))
140110220425032 140110423481608
# 即使是物件中的可變物件,也會重新生成一份
In [79]: print([id(element) for element in will])
[140110445106880, 10912032, 140110220811400]
In [80]: print([id(element) for element in wilber])
[140110445106880, 10912032, 140110220285128]
In [81]: will[0] = 'wilber'
In [82]: will[2].append('java')
In [83]: will, wilber
Out[83]:
(['wilber', 28, ['python', 'c#', 'javascript', 'css', 'java']],
['will', 28, ['python', 'c#', 'javascript', 'css']])
In [84]: print([id(element) for element in will])
[140110221716032, 10912032, 140110220811400]
In [85]: print([id(element) for element in wilber])
[140110445106880, 10912032, 140110220285128]
引用計數
當引用計數為0
時,將立即回收該物件記憶體,要麼將對應的block
標記為空閒,要麼返還給作業系統,在回收時會呼叫__del__
方法:
>>> class User(object):
... def __del__(self):
... print "dead"
...
>>> a = User()
>>> sys.getrefcount(a)
2
>>> del a
dead
某些內建型別,比如小整數,因為快取的緣故,計數永遠不會為0,直到程序結束才由虛擬機器清理函式釋放。
python
還支援弱引用,允許在不增加引用計數,不妨礙物件回收的情況下間接引用物件。但是list
、dict
弱引用會引發異常。
弱引用
引用計數無法回收迴圈引用的物件,因此在物件群組內部使用弱引用有時能避免出現引用迴圈。它與強引用相對,是指不能確保其引用的物件不會被垃圾回收器回收的引用。一個物件若只被弱引用所引用,則可能在任何時候被回收。弱引用的主要作用就是減少迴圈時引用,減少記憶體中不必要的物件存在的數量。
建立弱引用可以通過weakref
模組的ref(obj[,callback])
來建立弱引用,obj
是想弱引用的物件,callback
是一個可選的函式。當沒有引用導致python
要銷燬這個物件時,呼叫callback
,callback
要求單個引數(弱引用的物件)。
>>> import sys
>>> import weakref
>>> class Man:
... pass
...
>>> o = Man()
>>> sys.getrefcount(o)
2
>>> r = weakref.ref(o)
>>> sys.getrefcount(o)
2
>>> r
<weakref at 0x7f14e2558a48; to 'instance' at 0x7f14e24dc830>
>>> o1 = r()
>>> o1 is o
True
>>> sys.getrefcount(o)
3
>>> o = None
>>> o1 = None
>>> sys.getrefcount(o)
2548
>>> r
<weakref at 0x7f14e2558a48; dead>
垃圾回收
除了引用計數,還有專門處理迴圈引用的GC
。一般能引發迴圈引用問題的,都是容器類物件,例如list
、set
、object
。當不存在迴圈引用時,理論上可以禁用GC
。
>>> import gc
>>> gc.disable()
>>> class User(object): pass
...
>>> def callback(r): print r, "dead"
...
>>> gc.disable()
>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a;
>>> del a; del b;
>>> wa(), wb()
(<__main__.User object at 0x7f14e24e1190>, <__main__.User object at 0x7f14e24e11d0>)
>>> gc.enable()
>>> gc.isenabled()
True
>>> gc.collect()
<weakref at 0x7f14e2558b50; dead> dead
<weakref at 0x7f14e2558ba8; dead> dead
11
GC
將要回收的物件分成3
級:
>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
>>> gc.get_count()
(263, 9, 0)
包含
__del__
方法的迴圈引用物件,永遠不會被GC
回收,直至程序終止。
>>> class User(object):
... def __del__(self): pass
...
>>> def callback(r): print r, "dead!"
...
>>> gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # 輸出詳細的回收狀態資訊
>>> gc.isenabled()
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: 10 0 3637
gc: uncollectable <User 0x7f14e24e1210> # a
gc: uncollectable <User 0x7f14e24e1250> # b
gc: uncollectable <dict 0x7f14e2550b40> # a.__dict__
gc: uncollectable <dict 0x7f14e2550910> # b.__dict__
gc: done, 4 unreachable, 4 uncollectable, 0.0023s elapsed.
4
編譯
要執行python
程式,必須將原始碼編譯成位元組碼。通常情況下,編譯器會將原始碼轉化成位元組碼後儲存在pyc
檔案中。編譯發生在模組載入那一刻。
-
載入
pyc
流程:- 核對檔案
Magic
標記(由Python
版本號計算得來) - 檢查時間戳和原始碼檔案修改時間是否相同,以確定是否需要重新編譯
- 載入模組
- 核對檔案
-
如果沒有
pyc
,需要先進行編譯- 對原始碼進行
AST
分析 - 將分析結果編譯成
PyCodeObject
- 將
Magic
、原始碼檔案修改時間、PyCodeObject
儲存到pyc
檔案中 - 載入模組
- 對原始碼進行
PyCodeObject
包含了程式碼物件的完整資訊。內部成員嵌入到co_consts
列表中。