從c代碼分析可知,python全部對象的內存有着一樣的起始結構:引用計數+類型信息,實際上這些信息在python本體重也是能夠透過包來一窺一二的,python
from ctypes import * class PyObject(Structure): _fields_ = [("refcnt", c_size_t), ("typeid", c_void_p)] a = "this is a string" # 經過id(a)能夠得到對象a的內存地址,而PyObject.from_address()能夠將 # 指定的內存地址的內容轉換爲一個PyObject對象。經過此PyObject對象obj_a # 能夠訪問對象a的結構體中的內容。 obj_a = PyObject.from_address(id(a)) print(obj_a.refcnt) b = [a]*10 print(obj_a.refcnt)
查看對象a的引用次數,原文中返回值1,實際返回2,多是python3相對2的改動使得多引用一次,接下來建立一個列表,此列表中的每一個元素都是對象a,所以此列表應用了它10次,因此引用次數變爲了12。數組
查看對象a的類型對象的地址,它和id(type(a))相同,而因爲對象a的類型爲str,所以也就是id(str),app
print(obj_a.typeid) print(id(type(a))) print(id(str))
1825992064測試
1825992064ui
1825992064this
驗證一下,從這個地址讀取python對象試試,spa
obj_s = PyObject.from_address(id(str)) print(obj_s.typeid)
1825980464指針
指向新的地址,若是這個地址對應python的類型對象,那麼它的類型應該是元類對象,咱們嘗試一下在源碼解析一書上的知識。對象
print(id(type)) type_obj = PyObject.from_address(id(type)) print(type_obj.typeid)
1825980464blog
1825980464
符合,str(對應字符串類)的類型、元類自己的類型的、元類自己,三者的地址都指向同一位置。
這種對象除了有PyObject中的兩個字段以外,還有一個val字段保存實際的值,二者佔用空間也就不一樣,這種不可變對象的內存值部分與頭信息是相連的,因此使用sys.getsizeof能夠一併查到:
import sys print(sys.getsizeof(1)) print(sys.getsizeof(1.0))
28
24
# 在定義長度不固定的字段時,使用長度爲0的數組定義一個不佔內存的 # 僞字段_val。create_var_object()用來建立大小不固定的結構體對象 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_: # 首先搜索名爲_val的字段,並將其類型保存到inner_type中 if name == "_val": inner_type = t._type_ print(inner_type) if inner_type is not None: # 而後建立一個PyVarObject結構體讀取obj對象中的size字段 tmp = PyVarObject.from_address(id(obj)) size = tmp.size # 再經過size字段的大小建立一個對應的Inner結構體類,它可 # 以從struct繼承,由於struct中的_val字段不佔據內存。 class Inner(struct): _fields_ = [("val", inner_type*size)] Inner.__name__ = struct.__name__ struct = Inner return struct.from_address(id(obj)) s = 'asdfgh' s_obj = create_var_object(PyStr, s) s_obj.size
因爲只是模擬,咱們使用一個新建的僞字符串結構去從真的字符串結構讀取數據格式化解析,並建立新對象。
l = 0x1234567890abcd l_obj = create_var_object(PyLong, l) print(l_obj.size) val = list(l_obj.val) print(val)
<class 'ctypes.c_ushort'>
2
[43981, 14480]
不管是幾進制,實際l的type就是int。這裏面咱們看到了,實際上咱們使用了多個普通的整形數表達一個很大的值,長整型其實是一個序列。
列表對象的長度是可變的,所以不能採用字符串那樣的結構體,
而是使用了一個指針字段items指向可變長度的數組,而這個數組自己是一個指向PyObject的指針。allocated字段表示這個指針數組的長度,而size字段表示指針數組中已經使用的元素個數,即列表的長度。列表結構體自己的大小是固定的。
class PyList(PyVarObject): # item字段是指針數組,這裏表現爲二級指針 _fields_ = [("items", POINTER(POINTER(PyObject))), ("allocated", c_size_t)] def print_field(self): # size字段表示指針數組中已經使用的元素個數 # allocated字段表示這個指針數組的長度 # 指針字段items指向可變長度的數組 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 range(10): alist_obj.print_field() alist.append(x) test_list()
一開始列表的長度和其指針數組的長度都是3,即列表處於飽和狀態。所以向列表中添加新元素時,須要從新分配指針數組,所以指針數組的長度變爲了7,而地址也發生了變化。這時列表的長度爲4,所以指針數組中還有3個空位保存新的元素。因爲每次從新分配指針數組時,都會預分配一些額外空間,所以往列表中添加元素的平均時間複雜度爲O(1),
3 3 <cparam 'P' (000001E0A1ABB330)> 4 7 <cparam 'P' (000001E09E0B14D0)> 5 7 <cparam 'P' (000001E09E0B14D0)> 6 7 <cparam 'P' (000001E09E0B14D0)> 7 7 <cparam 'P' (000001E09E0B14D0)> 8 12 <cparam 'P' (000001E0A0832AF0)> 9 12 <cparam 'P' (000001E0A0832AF0)> 10 12 <cparam 'P' (000001E0A0832AF0)> 11 12 <cparam 'P' (000001E0A0832AF0)> 12 12 <cparam 'P' (000001E0A0832AF0)>
刪除元素原理一致,
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()
10000 10000 <cparam 'P' (000001E0A24E63B0)> 10 17 <cparam 'P' (000001E0A24E63B0)>