我们在学习理解一个事物时,往往遵循着由表及里的规律。第一步,我们学习一个事物的特性表现(feature)。在对事物的表现有了掌握以后,进一步的,我们尝试探索事物表现背后的内在原理。本文在叙述过程中,也遵循这样一个过程。第一部分是对python对象系统feature的简要概述,第二部分则是对python 对象系统底层实现的梳理与总结,在文章的最后一部分,是对常用的内置python类型实现的一个介绍。
Python 中的对象与类型的行为表现
我们在学习python的过程中,比较经常听到的一句话叫做 ,python处处皆对象。这是对于python对象模型的高度概括的描述。Python中的变量,实际上是一个名字,指向所引用的对象。Python中有“名字空间”的概念,这类似于其他编程语言中的作用域。在python中,名字空间底层由一个dict实现,变量名就是字典中的键,而变量引用的对象就是字典中键对应的值。
那么现在我们知道,不同于其他的语言,python中的整数,布尔值,字符串等类型的变量均是一个对象而不是简单的值。更进一步的,python中的所有语法元素,全部都是对象。函数是函数对象,模块是模块对象,最特殊的是,对象的类型也是一个对象。
Python中的面向对象机制与java等语言,所有的类型默认继承自基类object,或者某个object的派生类型。但是由于在python中类型也是一个对象,所以包含基类object在内的所有类型,都是对象实例。区别于普通对象,我们姑且把描述类型的对象叫做 类对象。所有类对象的类型被叫做元类(metaclass),元类默认是一个type类对象,或者某个继承自type的类对象。最后,type类对象的类型是type自身。
类对象总结:
类对象 | 基类 | 类对象的类型 |
---|---|---|
普通类型 | object/object 派生类 | Type/type 派生类 |
元类 | type | type |
Object(默认基类) | object | type |
Type(默认元类) | object | type |
Python 底层对于对象与类型的实现
在对python语言层面上的对象与类型(对象)的行为表现有了了解以后,下面我们来研究一下,在python的底层是如何用c语言实现python对象与python类型的。
对象
python对象在python实现中,用PyObject实例来描述。Python中的对象有很多种,int类型的对象对应于PyIntObject实例,string类型的对象对应于PyStringObject实例,但是这些PyxxxObject实例同时又都是PyObject实例,假设我们用c++去实现python的话,那么显而易见的是,PyObject是一个基类,各种不同类型的对象实例描述都继承自PyObject。然而,python是由c去实现的,没有类与继承的概念,python的开发者通过精心设计结构体的成员和布局模拟了继承与多态。首先泛化的PyObject结构本身非常简单,共有两个成员ob_refcnt,和ob_type两个成员,其结构如下图:
其中ob_refcnt 是对象的引用计数,而ob_type指向了对象的类型。
我们再来看一个特化object结构,PyIntObject。它由三个成员组成 ob_refcnt,ob_type,ob_val,其结构如下图:
可以看到,PyIntObject的头,就是PyObject结构,这意味着,我们可以通过PyObject* 去引用一个PyIntObject实例。这就实现了在c++中通过基类指针去引用和访问派生类对象的多态特性。更进一步的说,所有的python对象,都包含一个PyObject头,其中存储着这个对象本身的引用计数和所述类型。不同类型的对象可能拥有不同类型的成员,这些成员会存贮在对象内存空间中PyObject头后面的内存里。
Python 对象可以概括的分为两种,定长对象和非定长对象。定长对象指的是这类对象的数据成员长度全部相同,反之,两个同类非定长对象的数据成员是可以不同的。显然,Python int对象就是一个定长python对象,而python string对象就是非定长对象。因为两个python string对象的长度是不相同的。对于非定长的python对象,其内存头部除了保存引用计数和类型之外,还保存了该对象实例本身的数据长度ob_size。
定长对象结构:
变长对象结构:
类型
Python类型在python实现中,用PyTypeObject 实例来描述。Python类型有很多种,每一种类型,都用一个PyTypeObject实例来描述。例如整数类型用PyInt_Type描述,string类型用PyString_Type描述,PyTypeObject中主要存贮了特定Python类型的方法,方法以函数指针的形式存储在PyTypeObject的结构里。
首先应该注意到的是,描述python类型的数据结构中,竟然有一个非定长python对象头,这恰恰印证了,python类型本身也是对象的说法。仔细观察pyTypeObject数据结构,其中大部分的成员是用于保存类型特殊方法的默认的c语言实现的。举例来说tp_new,对应于new 的实现,tp_init 对应于init 的实现。关于类型的方法,在这里暂且按下不表,我们首先关注pyTypeObject中的ob_type 和tp_bases两个成员。Ob_type 成员用于指向一个python对象的python类型。那么作为类对象,其类型就是元类,所以,一个用于描述某种python类型的pyTypeObject实例,其Ob_type 指向的是这个python类型的元类所对应的pyTypeObject实例。默认情况下python类型的元类是type类型,其对应的pyTypeObject实例为 pyType_Type。 tp_bases 指向一个数组,其中存储了python类型的直接基类。如果一个python类型直接继承自object(对应的pyTypeObject 实例为pyBaseObject_Type),那么其对应的pyTypeObject实例的tp_bases 成员中只含有一个指向pyBaseObject_Type 对象的指针。
object与type
现在我们已经知道了,在python类型中有两个特殊的类型,object和type。在python底层分别用pyBaseObject_Type和pyType_Type两个pyTypeObject实例描述。
Object和type的元类都是type,这意味着,pyBaseObject_Type和pyType_Type的obtype都指向pyType_Type。
Object和type的基类都是object,这意味着,pyBaseObject_Type和pyType_Type的tb_bases中都有且只有一个指向pyBaseObject_Type的指针。
python对象系统小结
到此,对于python对象与python类型之间的基本关系可以算是梳理清楚了,在这个基础之上,之后还可以进一步去探索探索自定义类型的创建,类型实例化等内容。但是在这之前,这里我们用一个示意图描述python对象与类型之间的关系。
部分内置python类型对象实现介绍
当我们使用python进行开发时,最长打交道的是各种内置类型的对象。了解常用内置类型的对象实例在python层是如何实现的,有助于我们更加高效的使用python。这一部分,我将整理python最常用的内置类型的对象的底层实现。
int
Python 底层使用PyIntObject 结构来描述python int 对象。PyIntObject是一个定长的python对象,其构造十分简单,只含有一个成员ob_val,保存的就是python int 实例中的数值。
从PyIntObject数据结构可知,Python使用一个long 类型来保存数值。从日常使用python的经验可以知道,python的整数类型是不用担心溢出问题的,然而这里如果单单只用一个long类型去保存数值的话,那么是可能出现溢出情况的。对于这种情况python底层做了透明的类型转换,一旦一个pyIntObject对象包含的数值会发生溢出,那么就重新创建一个pyLongObject 对象去保存数值。
对于常用的python内置对象,另一个不得不提的事情是对象缓存机制。由于python处处皆对象,这意味着为python内置对象申请内存空间的操作将会非常频繁。为了提高性能,针对于大部分常用的python内置类型对象,python底层都有相应的缓存措施。
Python整数对象有两个层次的缓冲措施。第一个措施是针对于小整数。在python2.7中,
范围在[-5,257]的整数对象,一旦创建将永久缓存在系统中。具体的存放位置是一个叫做small_ints的数组。针对于普通的整数对象,python设计了内存块空闲链表缓存所有malloc出的内存并加以管理,这大大减少了由整数对象创建引发malloc的几率。
long
Python 底层使用PyLongObject 结构去描述python的大整数对象。PyLongObject内部用一个数组去保存数值,因此PyLongObject是一个边长对象.
在python源码的注释中,可以看到如何用这个数组去存储一个大整数
“ The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2*(SHIFTi)”
bool
在python中,Bool对象只有两个对象实例Py_True 和Py_False。这是两个PyIntObject对象,数据成员的值分别为1和0。
string
Python用PyStringObject 来描述字符串对象,python字符串对象也是一个变长的数据对象。除了变长对象头,字符串对象还包含了字符数组和字符串常量的哈希值,此外对象内部还有一个state标志位用来标识这个对象有没有被python的intern缓存机制处理。
同整数对象一样,python的字符串对象也有两种缓存机制。分别是针对单个字符的缓冲数组,和针对于所有字符串(包含字符)的intern机制。
Python中所有长度为1的字符串(字符),一旦被创建,就会被永久的缓存到系统中,存储在一个名字为characters的数组里。
而python的字符串intern机制,保证了任何两个字符串变量,如果其字符串内容相同,那么两个名字引用的是用一个字符串对象。
list
Python用PyListObject来描述列表对象,PyListObject的数据结构如下图:
从上图可以看出,PyListObject看起来是一个变长的pyhon对象,从直观理解上,列表对象是变长的这个事情也是说的通的,但是个人在这里倾向于认为PyListObject是定长的列表对象。列表对象维护的数据对象都存储在由ob_item指向的空间,而这段空间与PyListOBject是分离的!所以PyListObject本身应该是定长的。Ob_size在这里用于指出列表当前有多少个对象,allocated用于描述Ob_item所指向的内存空间的总长度。当ob_size等于allocated时意味着内存已满,需要重新分配一块内存给这个列表对象,这个过程与c++ vector容器是一致的。
dict
Python 用PyDictObject来描述字典对象,PyDictObject的数据结构如下图:
可以看到,PyDictObject 一个定长的python对象,注意到PyDictObject中有两个重要的成员,Ma_table 和Ma_smalltable。Ma_table 的作用和list对象中Ob_item的作用是一样的,指向的是真正保存字典元素的空间,当字典中的元素个数小于8时,这片空间就是Ma_smalltable数组!而当字典元素大于8时,系统会另外申请新的内存空间,并将ma_smalltable的内容拷贝过去,最后使ma_table指向新的内存空间。
PyDictObject 保存的元素是一个pyDictEntry, 显然这是一个键值对,其结构如下
可以看到的是,无论是键还是值,保存在PyDictObject中的都仅仅是一个指针。
PyDictObject除了作为python层dict对象的描述,在python底层的实现中,应用也极为广泛。例如string intern缓存,locals 命名空间都是用PyDictOBject实现的。因此其对于搜索的效率要求很高。为此PydictObject采用了散列表的方式存储元素以实现O(1) 复杂度的查找操作,而不是像c++ map容器一样使用红黑树结构。PythonDict容器对象的散列,采用的是开放定址法去实现。当产生散列冲突时,通过二次探测函数,发现下一个候选位置。