在 Python 中一切皆是物件,而在實現 Python 的 C
語言中,這些物件只不過是一些比較複雜的結構體而已。本文通過 ctypes
訪問物件對應的結構體中的資料,加深對 Python 物件的理解。
物件的兩個基本屬性
Python 所有物件結構體中的頭兩個欄位都是相同的:
refcnt
:物件的引用次數,若引用次數為0
則表示此物件可以被垃圾回收了。typeid
:指向描述物件型別的物件的指標。通過
ctypes
,我們可以很容易定義一個這樣的結構體:PyObject
。注意:本文只描述在
32位作業系統下的情況,如果讀者使用的是
64位作業系統,需要對程式中的一些欄位型別做一些改變。
1 2 3 4 5 |
from ctypes import * class PyObject(Structure): _fields_ = [("refcnt", c_size_t), ("typeid", c_void_p)] |
下面讓我們用 PyObject
做一些實驗幫助理解這兩個欄位的含義:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> a = "this is a string" >>> obj_a = PyObject.from_address(id(a)) ❶ >>> obj_a.refcnt ❷ 1L >>> b = [a]*10 >>> obj_a.refcnt ❸ 11L >>> obj_a.typeid ❹ 505269056 >>> id(type(a)) 505269056 >>> id(str) 505269056 |
❶通過 id(a)
可以獲得物件 a 的記憶體地址,而 PyObject.from_address()
可以將指定的記憶體地址的內容轉換為一個 PyObject
物件。通過此 PyObject
物件obj_a
可以訪問物件 a 的結構體中的內容。
❷檢視物件 a
的引用次數,由於只有 a 這個名字引用它,因此值為 1。接下來建立一個列表,此列表中的每個元素都是物件 a,因此此列表應用了它 10 次,❸所以引用次數變為了 11。
❸檢視物件 a 的型別物件的地址,它和 id(type(a))
相同,而由於物件a的型別為str
,因此也就是 id(str)
。
下面檢視str型別物件的這兩個欄位:
1 2 3 4 5 6 7 |
>>> obj_str = PyObject.from_address(id(str)) >>> obj_str.refcnt 252L >>> obj_str.typeid 505208152 >>> id(type) 505208152 |
可以看到 str
的型別就是type
。再看看 type
物件:
1 2 3 |
>>> type_obj = PyObject.from_address(id(type)) >>> type_obj.typeid 505208152 |
type
物件的型別指標就指向它自己,因為 type(type) is type
。
整數和浮點數物件
接下來看看整數和浮點數物件,這兩個物件除了有 PyObject
中的兩個欄位之外,還有一個 val
欄位儲存實際的值。因此 Python
中一個整數佔用 12 個位元組,而一個浮點數佔用 16 個位元組:
1 2 3 4 |
>>> sys.getsizeof(1) 12 >>> sys.getsizeof(1.0) 16 |
我們無需重新定義 refcnt
和 typeid
這兩個欄位,通過繼承 PyObject
,可以很方便地定義整數和浮點數對應的結構體,它們會繼承父類中定義的欄位:
1 2 3 4 5 |
class PyInt(PyObject): _fields_ = [("val", c_long)] class PyFloat(PyObject): _fields_ = [("val", c_double)] |
下面是使用 PyInt
檢視整數物件的例子:
1 2 3 4 5 6 |
>>> i = 2000 >>> i_obj = PyInt.from_address(id(a)) >>> i_obj.refcnt 1L >>> i_obj.val 2000 |
通過 PyInt
物件,還可以修改整數物件的內容:
修改不可變物件的內容會造成嚴重的程式錯誤,請不要用於實際的程式中。
1 2 3 4 |
>>> j = i >>> i_obj.val = 2012 >>> j 2012 |
由於i和j引用的是同一個整數物件,因此i和j的值同時發生了變化。
結構體大小不固定的物件
表示字串和長整型數的結構體的大小不是固定的,這些結構體在 C 語言中使用了一種特殊的欄位定義技巧,使得結構體中最後一個欄位的大小可以改變。由於結構體需要知道最後一個欄位的長度,因此這種結構中包含了一個 size
欄位,儲存最後一個欄位的長度。在 ctypes
中無法表示這種長度不固定的欄位,因此我們使用了動態建立結構體類的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class PyVarObject(PyObject): _fields_ = [("size", c_size_t)] class PyStr(PyVarObject): _fields_ = [("hash", c_long), ("state", c_int), ("_val", c_char*0)] ❶ class PyLong(PyVarObject): _fields_ = [("_val", c_uint16*0)] def create_var_object(struct, obj): inner_type = None for name, t in struct._fields_: if name == "_val": ❷ inner_type = t._type_ if inner_type is not None: tmp = PyVarObject.from_address(id(obj)) ❸ size = tmp.size class Inner(struct): ❹ _fields_ = [("val", inner_type*size)] Inner.__name__ = struct.__name__ struct = Inner return struct.from_address(id(obj)) |
❶在定義長度不固定的欄位時,使用長度為 0
的陣列定義一個不佔記憶體的偽欄位 _val
。 create_var_object()
用來建立大小不固定的結構體物件,❷首先搜尋名為 _val
的欄位,並將其型別儲存到 inner_type
中。❸然後建立一個PyVarObject
結構體讀取obj物件中的 size
欄位。❹再通過 size 欄位的大小建立一個對應的 Inner
結構體類,它可以從 struct
繼承,因為 struct
中的 _val
欄位不佔據記憶體。
下面我們用上面的程式做一些實驗:
1 2 3 4 5 |
>>> s_obj = create_var_object(PyStr, s) >>> s_obj.size 9L >>> s_obj.val 'abcdegfgh' |
當整數的範圍超過了 0x7fffffff
時,Python 將使用長整型整數:
1 2 3 4 5 6 7 |
>>> l = 0x1234567890abcd >>> l_obj = create_var_object(PyLong, l) >>> l_obj.size 4L >>> val = list(l_obj.val) >>> val [11213, 28961, 20825, 145] |
可以看到 Python 用了 4 個 16 位的整數表示 0x1234567890abcd
,下面我們看看長整型數是如何用陣列表示的:
1 2 |
>>> hex((val[3] << 45) + (val[2] << 30) + (val[1] << 15) + val[0]) '0x1234567890abcdL' |
即陣列中的後面的元素表示高位,每個 16 為整數中有 15 位表示數值。
列表物件
列表物件的長度是可變的,因此不能採用字串那樣的結構體,而是使用了一個指標欄位items指向可變長度的陣列,而這個陣列本身是一個指向 PyObject
的指標。 allocated
欄位表示這個指標陣列的長度,而 size
欄位表示指標陣列中已經使用的元素個數,即列表的長度。列表結構體本身的大小是固定的。
1 2 3 4 5 6 |
class PyList(PyVarObject): _fields_ = [("items", POINTER(POINTER(PyObject))), ("allocated", c_size_t)] def print_field(self): print self.size, self.allocated, byref(self.items[0]) |
我們用下面的程式檢視往列表中新增元素時,列表結構體中的各個欄位的變化:
1 2 3 4 5 6 7 |
def test_list(): alist = [1,2.3,"abc"] alist_obj = PyList.from_address(id(alist)) for x in xrange(10): alist_obj.print_field() alist.append(x) |
執行 test_list()
得到下面的結果:
1 2 3 4 5 6 7 8 9 10 11 |
>>> test_list() 3 3 <cparam 'P' (02B0ACE8)> ❶ 4 7 <cparam 'P' (028975A8)> ❷ 5 7 <cparam 'P' (028975A8)> 6 7 <cparam 'P' (028975A8)> 7 7 <cparam 'P' (028975A8)> 8 12 <cparam 'P' (02AAB838)> 9 12 <cparam 'P' (02AAB838)> 10 12 <cparam 'P' (02AAB838)> 11 12 <cparam 'P' (02AAB838)> 12 12 <cparam 'P' (02AAB838)> |
❶一開始列表的長度和其指標陣列的長度都是 3,即列表處於飽和狀態。因此❷往列表中新增新元素時,需要重新分配指標陣列,因此指標陣列的長度變為了 7,而地址也發生了變化。這時列表的長度為 4,因此指標陣列中還有 3 個空位儲存新的元素。由於每次重新分配指標陣列時,都會預分配一些額外空間,因此往列表中新增元素的平均時間複雜度為 O(1)
。
下面再看看從列表刪除元素時,各個欄位的變化:
1 2 3 4 5 6 7 |
def test_list2(): alist = [1] * 10000 alist_obj = PyList.from_address(id(alist)) alist_obj.print_field() del alist[10:] alist_obj.print_field() |
執行test_list2()得到下面的結果:
1 2 3 |
>>> test_list2() 10000 10000 10 17 |
可以看出大指標陣列的位置沒有發生變化,但是後面額外的空間被回收了。