『Python』源码解析_从ctype模块理解对象
1、对象的引用计数
从c代码分析可知,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。
2、对象的类型
查看对象a的类型对象的地址,它和id(type(a))相同,而由于对象a的类型为str,因此也就是id(str),
print(obj_a.typeid) print(id(type(a))) print(id(str))
1825992064
1825992064
1825992064
验证一下,从这个地址读取python对象试试,
obj_s = PyObject.from_address(id(str)) print(obj_s.typeid)
1825980464
指向新的地址,如果这个地址对应python的类型对象,那么它的类型应该是元类对象,我们尝试一下在源码解析一书上的知识。
3、元类的特殊性质
print(id(type)) type_obj = PyObject.from_address(id(type)) print(type_obj.typeid)
1825980464
1825980464
符合,str(对应字符串类)的类型、元类本身的类型的、元类本身,三者的地址都指向同一位置。
4、不可变对象值存储
这种对象除了有PyObject中的两个字段之外,还有一个val字段保存实际的值,两者占用空间也就不同,这种不可变对象的内存值部分与头信息是相连的,所以使用sys.getsizeof可以一并查到:
import sys print(sys.getsizeof(1)) print(sys.getsizeof(1.0))
28
24
5.1、扁平序列的创建
- 指定创建元素类型(字符串为字符,数组为int或者float)
- 获取目标对象长度(即几个数字,几个字符)
- 有了这两个信息就可以划分内存创建对象了
- 原数据就存储在内存中,我们指定结构体去赋予新格式,并加入新的对象头信息
模拟C语言创建扁平序列的过程,
# 在定义长度不固定的字段时,使用长度为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
由于只是模拟,我们使用一个新建的伪字符串结构去从真的字符串结构读取数据格式化解析,并创建新对象。
5.2、(伪)长整型数字
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。这里面我们看到了,实际上我们使用了多个普通的整形数表达一个很大的值,长整型实际上是一个序列。
6、List列表类型
列表对象的长度是可变的,因此不能采用字符串那样的结构体,
而是使用了一个指针字段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)>