轉載地址:http://hyry.dip.jp/tech/slice/slice.html/10html
在 Python 中一切皆是對象,而在實現 Python 的 C
語言中,這些對象只不過是一些比較複雜的結構體而已。本文經過 ctypes
訪問對象對應的結構體中的數據,加深對 Python 對象的理解。數組
Python 全部對象結構體中的頭兩個字段都是相同的:app
refcnt
:對象的引用次數,若引用次數爲 0
則表示此對象能夠被垃圾回收了。ui
typeid
:指向描述對象類型的對象的指針。this
經過 ctypes
,咱們能夠很容易定義一個這樣的結構體:PyObject
。操作系統
注意:本文只描述在
32 位操做系統下的狀況,若是讀者使用的是
64 位操做系統,須要對程序中的一些字段類型作一些改變。
指針
from ctypes import * class PyObject(Structure): _fields_ = [("refcnt", c_size_t), ("typeid", c_void_p)]
下面讓咱們用 PyObject
作一些實驗幫助理解這兩個字段的含義:code
>>> 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)
。htm
下面查看str類型對象的這兩個字段:對象
>>> obj_str = PyObject.from_address(id(str)) >>> obj_str.refcnt 252L >>> obj_str.typeid 505208152 >>> id(type) 505208152
能夠看到 str
的類型就是type
。再看看 type
對象:
>>> type_obj = PyObject.from_address(id(type)) >>> type_obj.typeid 505208152
type
對象的類型指針就指向它本身,由於 type(type) is type
。
接下來看看整數和浮點數對象,這兩個對象除了有 PyObject
中的兩個字段以外,還有一個 val
字段保存實際的值。所以 Python
中一個整數佔用 12 個字節,而一個浮點數佔用 16 個字節:
>>> sys.getsizeof(1) 12 >>> sys.getsizeof(1.0) 16
咱們無需從新定義 refcnt
和 typeid
這兩個字段,經過繼承 PyObject
,能夠很方便地定義整數和浮點數對應的結構體,它們會繼承父類中定義的字段:
class PyInt(PyObject): _fields_ = [("val", c_long)] class PyFloat(PyObject): _fields_ = [("val", c_double)]
下面是使用 PyInt
查看整數對象的例子:
>>> i = 2000 >>> i_obj = PyInt.from_address(id(a)) >>> i_obj.refcnt 1L >>> i_obj.val 2000
經過 PyInt
對象,還能夠修改整數對象的內容:
修改不可變對象的內容會形成嚴重的程序錯誤,請不要用於實際的程序中。
>>> j = i >>> i_obj.val = 2012 >>> j 2012
因爲i和j引用的是同一個整數對象,所以i和j的值同時發生了變化。
表示字符串和長整型數的結構體的大小不是固定的,這些結構體在 C 語言中使用了一種特殊的字段定義技巧,使得結構體中最後一個字段的大小能夠改變。因爲結構體須要知道最後一個字段的長度,所以這種結構中包含了一個 size
字段,保存最後一個字段的長度。在 ctypes
中沒法表示這種長度不固定的字段,所以咱們使用了動態建立結構體類的方法。
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
字段不佔據內存。
下面咱們用上面的程序作一些實驗:
>>> s_obj = create_var_object(PyStr, s) >>> s_obj.size 9L >>> s_obj.val 'abcdegfgh'
當整數的範圍超過了 0x7fffffff
時,Python 將使用長整型整數:
>>> 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
,下面咱們看看長整型數是如何用數組表示的:
>>> hex((val[3] << 45) + (val[2] << 30) + (val[1] << 15) + val[0]) '0x1234567890abcdL'
即數組中的後面的元素表示高位,每一個 16 爲整數中有 15 位表示數值。
列表對象的長度是可變的,所以不能採用字符串那樣的結構體,而是使用了一個指針字段items指向可變長度的數組,而這個數組自己是一個指向 PyObject
的指針。 allocated
字段表示這個指針數組的長度,而 size
字段表示指針數組中已經使用的元素個數,即列表的長度。列表結構體自己的大小是固定的。
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])
咱們用下面的程序查看往列表中添加元素時,列表結構體中的各個字段的變化:
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()
獲得下面的結果:
>>> 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)
。
下面再看看從列表刪除元素時,各個字段的變化:
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()獲得下面的結果:
>>> test_list2() 10000 10000 <cparam 'P' (034E5AB8)> 10 17 <cparam 'P' (034E5AB8)>
能夠看出大指針數組的位置沒有發生變化,可是後面額外的空間被回收了。