1. 程式人生 > 實用技巧 >Python拾遺(1)

Python拾遺(1)

讀《Python原始碼分析》的一些記錄。

虛擬機器

python會在模組載入時將原始碼編譯成位元組碼,然後位元組碼會被虛擬機器在核心函式中解釋執行。
虛擬機器開始執行時,通過初始化函式完成整個執行環境設定,包括:

  • 建立__builtlin__模組,持有所有內建型別和函式
  • 建立sys模組
  • 初始化內建Exception
  • 建立__main__模組,準備所需名字空間
  • 通過site.pysite-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模組中找到,intlongstr都可以看做是簡短別名:

>>> 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函式堆疊幀名稱空間,以及classinstance等。

>>> 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還支援弱引用,允許在不增加引用計數,不妨礙物件回收的情況下間接引用物件。但是listdict弱引用會引發異常。

弱引用

引用計數無法回收迴圈引用的物件,因此在物件群組內部使用弱引用有時能避免出現引用迴圈。它與強引用相對,是指不能確保其引用的物件不會被垃圾回收器回收的引用。一個物件若只被弱引用所引用,則可能在任何時候被回收。弱引用的主要作用就是減少迴圈時引用,減少記憶體中不必要的物件存在的數量。
建立弱引用可以通過weakref模組的ref(obj[,callback])來建立弱引用,obj是想弱引用的物件,callback是一個可選的函式。當沒有引用導致python要銷燬這個物件時,呼叫callbackcallback要求單個引數(弱引用的物件)。

>>> 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。一般能引發迴圈引用問題的,都是容器類物件,例如listsetobject。當不存在迴圈引用時,理論上可以禁用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列表中。