从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)>