02-一切对象皆PyObject
02-一切皆对象PyObject
一切皆对象PyObject
Python中一切皆对象,int str list dict tuple都是对象,类型也是对象。程序员可以通过class创建自己的对象,对象对于程序员来说是数据,对计算机来说是一块内存。
Python中还有一个特殊的类型(对象),叫做object,它是所有类型对象的基类。不管是什么类,内置的类也好,我们自定义的类也罢,它们都继承自object。因此,object是所有类型对象的”基类”、或者说”父类”。
我们说可以使用type和__class__查看一个对象的类型,并且还可以通过isinstance来判断该对象是不是某个已知类型的实例对象;那如果想查看一个类型对象都继承了哪些类该怎么做呢?我们目前都是使用issubclass来判断某个类型对象是不是另一个已知类型对象的子类,那么可不可以直接获取某个类型对象都继承了哪些类呢?
答案是可以的,方法有三种,我们分别来看一下:
1 | class A: pass |
__base__: 如果继承了多个类, 那么只显示继承的第一个类, 没有显示继承则返回一个<class 'object'>;
__bases__: 返回一个元组, 会显示所有直接继承的父类, 如果没有显示的继承, 则返回(<class 'object'>,);
__mro__: mro表示Method Resolution Order, 表示方法查找顺序, 会从自身除法, 找到最顶层的父类, 因此返回自身、继承的基类、以及基类继承的基类, 一直找到object;
最后我们来看一下type和object,估计这两个老铁之间的关系会让很多人感到困惑。
我们说type是所有类的元类,而object是所有的基类,这就说明type是要继承自object的,而object的类型是type。
这就怪了,这难道不是一个先有鸡还是先有蛋的问题吗?其实不是的,这两个对象是共存的,它们之间的定义其实是互相依赖的。至于到底是怎么肥事,我们后面在看解释器源码的时候就会很清晰了。
总之目前记住两点:
1. type站在类型金字塔的最顶端, 任何的对象按照类型追根溯源, 最终得到的都是type;
2. object站在继承金字塔的最顶端, 任何的类型对象按照继承追根溯源, 最终得到的都是object;
我们说type的类型还是type,但是object的基类则不再是object,而是一个None。为什么呢?其实答案很简单,我们说Python在查找属性或方法的时候,会回溯继承链,自身如果没有的话,就会按照__mro__指定的顺序去基类中查找。所以继承链一定会有一个终点,否则就会像没有出口的递归一样出现死循环了。
最后将上面那张关系图再完善一下的话:
实现对象机制的基石-PyOBject
根据对象的不同特点还可以进一步分类:
可变对象:对象创建之后可以本地修改;
不可变对象:对象创建之后不可以本地修改;
定长对象:对象所占用的内存大小固定;
不定长对象:对象所占用的内存大小不固定;
但是”对象”在Python的底层是如何实现的呢?我们知道标准的Python解释器是C语言实现的CPython,但C并不是一个面向对象的语言,那么它是如何实现Python中的面向对象的呢?
首先对于人的思维来说,对象是一个比较形象的概念,但对于计算机来说,对象却是一个抽象的概念。它并不能理解这是一个整数,那是一个字符串,计算机所知道的一切都是字节。通常的说法是:对象是数据以及基于这些数据的操作的集合。在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也可能是离散的。
而Python中的任何对象在C中都对应一个结构体实例,在Python中创建一个对象,等价于在C中创建一个结构体实例。所以Python中的对象本质上就是C中malloc函数为结构体实例在堆区申请的一块内存。
Python中一切皆对象,而所有的对象都拥有一些共同的信息(也叫头部信息),这些信息就在PyObject中,PyObject是Python整个对象机制的核心,我们来看看它的定义:
1 | //Include/object.h |
PyObject_HEAD_EXTRA
PyObject_HEAD_EXTRA 定义了两个双向链表,用于指向堆上创建的活着的对象
1 | /* Define pointers to support a doubly-linked list of all live heap objects. */ |
Ob_refcnt
ob_refcnt定义了引用计数器
当一个对象被引用时,那么ob_refcnt会自增1;引用解除时,ob_refcnt自减1。而一旦对象的引用计数为0时,那么这个对象就会被回收。
那么在哪些情况下,引用计数会加1呢?哪些情况下,引用计数会减1呢?
导致引用计数加1的情况:
对象被创建:比如name = "古明地觉", 此时对象就是"古明地觉"这个字符串, 创建成功时它的引用计数为1
变量传递使得对象被新的变量引用:比如Name = name
引用该对象的某个变量作为参数传到一个函数或者类中:比如func(name)
引用该对象的某个变量作为元组、列表、集合等容器的一个元素:比如lst = [name]
导致引用计数减1的情况:
引用该对象的变量被显示的销毁:del name
对象的引用指向了别的对象:name = "椎名真白"
引用该对象的变量离开了它的作用域,比如函数的局部变量在函数执行完毕的时候会被销毁
引用该对象的变量所在的容器被销毁,或者被从容器里面删除
所以我们使用del删除一个对象,并不是删除这个对象,我们没有这个权力,del只是使对象的引用计数减一,至于到底删不删是解释器判断对象引用计数是否为0决定的。为0就删,不为0就不删,就这么简单。
而ob_refcnt的类型是Py_ssize_t,在64位机器上直接把这个类型看成long即可(话说这都2020年了,不会还有人用32位机器吧)
,因此一个对象的引用计数不能超过long所表示的最大范围。但是显然,如果不是吃饱了撑的写恶意代码,是不可能超过这个范围的。
ob_type:类型指针
我们说一个对象是有类型的,类型对象描述实例对象的数据和行为,而ob_type存储的便是对应类型对象的指针,所以类型对象在底层对应的是struct _typeobject实例。从这里我们可以看出,所有的类型对象在底层都是由同一个结构体实例化得到的,因为PyObject是所有的对象共有的,它们的ob_type指向的都是struct _typeobject。
所以不同的实例对象对应不同的结构体,但是类型对象对应的都是同一个结构体。
因此我们看到PyObject的定义非常简单,就是一个引用计数和一个类型指针,所以Python中的任意对象都必有:引用计数和类型这两个属性。
实现变长对象的基石–PyVarObject
我们说PyObject是所有对象的核心,它包含了所有对象都共有的信息,但是还有那么一个属性虽然不是每个对象都有,但至少有一大半的对象会有,能猜到是什么吗?
我们说Python中的对象根据所占的内存是否固定可以分为定长对象和变长对象,而变长对象显然有一个长度的概念,比如字符串、列表、元组等等,即便是相同的实例对象,但是长度不同,所占的内存也是不同的。比如:字符串内部有多少个字符、元组、列表内部有多少个元素,显然这里的多少*也是Python中很多对象的共有特征,虽然不像引用计数和类型那样是每个对象都必有的,但也是相当大一部分对象所具有的。
所以针对变长对象,Python底层也提供了一个结构体,因为Python很多都是变长对象。
1 | //Include/object.h |
所以我们看到PyVarObject实际上是PyObject的一个扩展,它在PyObject的基础上提供了一个ob_size字段,用于记录内部的元素个数。比如列表,列表(PyListObject实例)
中的ob_size维护的就是列表的元素个数,插入一个元素,ob_size会加1,删除一个元素,ob_size会减1。所以我们使用len获取列表的元素个数是一个时间复杂度为O(1)的操作,因为ob_size是时刻都和内部的元素个数保持一致,使用len获取元素个数的时候会直接访问ob_size。
因此在Python中,所有的变长对象都拥有PyVarObject,而所有的对象都拥有PyObject,这就使得在Python中,对”对象”的引用变得非常统一,我们只需要一个PyObject *就可以引用任意一个对象,而不需要管这个对象实际是一个什么对象。所以在Python中,所有的变量、以及容器内部的元素,本质上都是一个PyObject *。
由于PyObject和PyVarObject要经常被使用,所以Python提供了两个宏,方便定义。
1 | //Include/object.h |
比如定长对象浮点数,在底层对应的结构体为PyFloatObject,只需在头部PyObject的基础上再加上一个double即可。
1 | //Include/Cpython/floatobject.h |
而对于变长对象列表,在底层对应的结构体是PyListObject,所以它需要在PyVarObject的基础上再加上一个指向数组的二级指针和一个容量即可。
1 | //Include/Cpython/listobject.h |
这上面的每一个成员都代表什么,我们之前已经分析过了。ob_item就是指向指针数组的二级指针,而allocated表示已经分配的容量,一旦添加元素的时候发现ob_size自增1之后会大于allocated,那么解释器就会对ob_item指向的指针数组进行扩容了。更准确的说,是申请一个容量更大数组,然后将原来指向的指针数组内部的元素按照顺序一个一个地拷贝到新的数组里面去,并让ob_item指向新的数组,这一点在分析PyListObject的时候会细说。所以我们看到列表在添加元素的时候,地址是不会改变的,即使容量不够了也没有关系,直接让ob_item指向新的数组就好了,至于PyListObject对象本身的地址是不会变化的。
最后再来介绍两个宏定义,这个是针对于类型对象的,我们后面在介绍类型对象的时候会经常见到这两个宏定义。
1 | // Include/object.h |
先看PyObject_HEAD_INIT,里面的_PyObject_EXTRA_INIT是用来实现refchain这个双向链表的,我们目前不需要管。里面的1指的是引用计数,我们看到刚创建的时候默认是设置为1的,至于type就是该类型对象的类型了,这个是作为宏的参数传进来的;而PyVarObject_HEAD_INIT,则是在PyObject_HEAD_INIT的基础之上,增加了一个size,显然我们从名字也能看出来这个size是什么。当然目前只是介绍这两个宏,先有个印象,类型对象的实现我们下面就会说。
实现类型对象的基石–PyTypeObject
通过PyObject和PyVarObject,我们看到了Python中所有对象的共有信息以及变长对象的共有信息。对于任何一个对象,不管它是什么类型,内部必有引用计数(ob_refcnt)
和类型指针(ob_type)
;对于任意一个变长对象,不管它是什么类型,除了引用计数和类型指针之外,内部还有一个表示元素个数的ob_size。
然目前是没有什么问题,一切都是符合我们的预期的,但是当我们顺着时间轴回溯的话,就会发现端倪。比如:
1. 当在内存中创建对象、分配空间的时候,解释器要给该对象分配多大的空间?显然不能随便分配,那么该对象的内存信息在什么地方?
2. 一个对象是支持相应的操作的,解释器怎么判断该对象支持哪些操作呢?再比如一个整型可以和一个整型相乘,但是一个列表也可以和一个整型相乘,即使是相同的操作,但不同类型的对象执行也会有不同的结果,那么此时解释器又是如何进行区分的?
想都不用想,这些信息肯定都在对象所对应的类型对象中。而且占用的空间大小实际上是对象的一个元信息,这样的元信息和其所属类型是密切相关的,因此它一定会出现在与之对应的类型对象当中。至于支持的操作就更不用说了,我们平时自定义类的时候,方法都写在什么地方,显然都是写在类里面,因此一个对象支持的操作显然定义在类型对象当中。
而将一个对象和其类型对象关联起来的,毫无疑问正是该对象内部的PyObject中的ob_type,也就是类型指针。我们通过对象的ob_type成员即可获取指向的类型对象的指针,通过该指针可以获取存储在类型对象中的某些元信息。
下面我们来看看类型对象在底层是怎么定义的:
1 | //Include/object.h |
类型对象在底层对应的是struct _typeobject,当然也是PyTypeObject,它里面的成员非常非常多,我们暂时挑几个重要的说,因为有一部分成员并不是那么重要,我们在后续会慢慢说。
目前我们了解到Python中的类型对象在底层就是一个PyTypeObject实例,它保存了实例对象的元信息,描述对象的类型。
Python中的实例对象在底层对应不同的结构体实例,而类型对象则是对应同一个结构体实例,换句话说无论是int、str、dict等等等等,它们在C的层面都是由PyTypeObject这个结构体实例化得到的,只不过成员的值不同PyTypeObject这个结构体在实例化之后得到的类型对象也不同。
我们看一下PyTypeObject内部几个非常关键的成员:
PyObject_VAR_HEAD:我们说这是一个宏,对应一个PyVarObject,所以类型对象是一个变长对象。而且类型对象也有引用计数和类型,这与我们前面分析的是一致的。
tp_name:类型的名称,而这是一个char *,显然它可以是int、str、dict之类的。
tp_basicsize, tp_itemsize:创建对应实例对象时所需要的内存信息。
tp_dealloc:其实例对象执行析构函数时所作的操作。
tp_print:其实例对象被打印时所作的操作。
tp_as_number:其实例对象为数值时,所支持的操作。这是一个结构体指针,指向的结构体中的每一个成员都是一个函数指针,其函数就是整型对象可以执行的操作,比如:四则运算、左移、右移、取模等等
tp_as_sequence:其实例对象为序列时,所支持的操作。同样是一个结构体指针。
tp_as_mapping:其实例对象为映射时,所支持的操作。也是一个结构体指针。
tp_base:继承的基类。
我们暂时就挑这么几个,事实上从名字上你也能看出来这每一个成员代表的含义。而且这里面的成员虽然多,但并非每一个类型对象都具备,比如int类型它就没有tp_as_sequence和tp_as_mapping,所以int类型的这两个成员的值都是0。
具体的我们就在分析具体的类型对象的时候再说吧,然后先来看看Python对象在底层都叫什么名字吧。
整型 -> PyLongObject结构体实例, int -> PyLong_Type(PyTypeObject结构体实例)
字符串 -> PyUnicodeObject结构体实例, str -> PyUnicode_Type(PyTypeObject结构体实例)
浮点数 -> PyFloatObject结构体实例, float -> PyFloat_Type(PyTypeObject结构体实例)
复数 -> PyComplexObject结构体实例, complex -> PyComplex_Type(PyTypeObject结构体实例)
元组 -> PyTupleObject结构体实例, tuple -> PyTuple_Type(PyTypeObject结构体实例)
列表 -> PyListObject结构体实例, list -> PyList_Type(PyTypeObject结构体实例)
字典 -> PyDictObject结构体实例, dict -> PyDict_Type(PyTypeObject结构体实例)
集合 -> PySetObject结构体实例, set -> PySet_Type(PyTypeObject结构体实例)
不可变集合 -> PyFrozenSetObject结构体实例, frozenset -> PyFrozenSet_Type(PyTypeObject结构体实例)
元类:PyType_Type(PyTypeObject结构体实例)
所以Python中的对象在底层的名字都遵循一定的标准,包括解释器提供的Python/C API也是如此。
下面以浮点数为例,考察一下类型对象和实例对象之间的关系。
1 | >>> float |
两个变量均指向了浮点数(PyFloatObject结构体实例),除了公共头部字段ob_refcnt和ob_type,专有字段ob_fval保存了对应的数值;浮点类型float则对应PyTypeObject结构体实例(PyFloat_Type),保存了类型名、内存分配信息以及浮点数相关操作。而将这两者关联起来的就是ob_type这个类型指针,它位于PyObject中,是所有对象共有的,而Python便是根据这个ob_type来判断该对象的类型,进而获取该对象的元信息。
我们说变量只是一个指针,那么int、float、dict这些是不是变量,显然是的,函数和类也是一个变量,所以它们在底层也是一个指针。只不过这些变量是内置的,直接指向了具体的PyTypeObject实例。只是为了方便,有时我们用int、float等等,来代指指向的对象。比如:float指向了底层的PyFloat_Type,所以它其实是PyFloat_Type的指针,但为了表述方便我们会直接用float来代指PyFloat_Type。
而且类型对象在解释器启动的时候就已经是创建好了的,不然的话我们怎么能够直接用呢?类型对象创建完毕之后,直接让float指向相应的类型对象。
我们来看一下float对应的类型对象在底层是怎么定义的吧。
1 | // Object/floatobject.c |
我们看到PyFloat_Type在源码中就直接被创建了,这是必须的,否则我们就没有办法直接访问float这个变量了,然后先看结构体中的第4行,我们看到tp_name被初始化成了”float”;第5行表示实例对象所占的字节数,我们看到就是一个PyFloatObject实例所占的内存大小,并且显然这个值是不会变的,说明无论创建多少个实例对象,它们的大小都是不变的,这也符合我们之前的测试结果,都是24字节。
再往下就是一些各种操作对应的函数指针,最后我们来看一下第3行,显然它接收的是一个PyVarObject,PyVarObject_HEAD_INIT这个宏无需赘言,但重点是里面的&PyType_Type,说明了float被设置成了type类型。
而且所有的类型对象(还有元类)在底层都被定义成了静态的全局变量,因为它们的声明周期是伴随着整个解释器的,并且在任意地方都可以访问。
模改CPython如何修改Type的打印信息?
例如,修改float的打印信息,进入到floatobject.h中,进入 PyAPI_DATA(PyTypeObject) PyFloat_Type; 中,进入(reprfunc)float_repr
重新编译CPython,发现打印的结果为:
类型对象的类型–PyType_Type
我们考察了float类型对象,知道它在C的层面是PyFloat_Type这个静态全局变量,它的类型是type,包括我们自定义的类的类型也是type。而type在Python中是一个至关重要的对象,它是所有类型对象的类型,我们称之为元类型(meta class)
,或者元类。借助元类型,我们可以实现很多神奇的高级操作。那么type在C的层面又长啥样呢?
在介绍PyFloat_Type的时候我们知道了type在底层对应PyType_Type,而它在”Object/typeobject.c”中定义,因为我们说所有的类型对象加上元类都是要预先定义好的,所以要源码中就必须要以静态全局变量的形式出现。
1 | //Object/typeobject.c |
我们所有的类型对象加上元类都是PyTypeObject这个结构体实例化得到的,所以它们内部的成员都是一样的,只不过传入的值不同,实例化之后的结果也不同,可以是PyLong_Type、可以是PyFloat_Type,也可以是这里的PyType_Type。
PyType_Type的内部成员和PyFloat_Type是一样的,但是我们还是要重点看一下里面的宏PyVarObject_HEAD_INIT,我们看到它传递的是一个&PyType_Type,说明它把自身的类型也设置成了PyType_Type,换句话说,PyType_Type里面的ob_type成员指向的还是PyType_Type。
1 | >>> type.__class__ |
显然不管我们套娃多少次,最终的结果都是True,显然这也是符合我们的预期的。
类型对象的基类–PyBaseObject_Type
我们说Python中有两个类型对象比较特殊,一个是站在类型金字塔顶端的type,一个是站在继承金字塔顶端的object。说完了type,我们来说说object,我们说类型对象内部的tp_base表示继承的基类,对于PyType_Type来讲,它内部的tp_base肯定是PyBaseObject_Type。
但令我们吃鲸的是,它的tp_base居然是个0,如果为0的话则表示没有这个属性。
1 | 0, /* tp_base */ |
不是说type的基类是object吗?为啥tp_base是0,事实上如果你去看PyFloat_Type的话,它内部的tp_base也是0。为0的原因就在于我们目前看到的类型对象是一个半成品,因为Python的动态性,显然不可能在定义的时候就将所有成员属性都设置好、然后解释器一启动就会得到我们平时使用的类型对象。目前看到的类型对象是一个半成品,有一部分成员属性是在解释器启动之后再进行动态完善的。
至于是怎么完善的,都有哪些成员需要解释器启动之后才能完善,我们后续系列会说。
而PyBaseObject_Type位于Object/object.c中,我们来一睹其芳容。
1 | //Object/object.c |
我们看到PyBaseObject_Type的类型也被设置成了PyType_Type,而PyType_Type类型在被完善之后,它的tp_base也会指向PyBaseObject_Type。所以之前我们说Python中的type和object是同时出现的,它们的定义是需要依赖彼此的。
1 | >>> object.__class__ |
注意:解释器在完善PyBaseObject_Type的时候,是不会设置其tp_base的,因为继承链必须有一个终点,否对象沿着继承链进行属性查找的时候就会陷入死循环,而object已经是继承链的顶点了。
1 | >>> print(object.__base__) |
object -> PyBaseObject_Type
object() -> PyBaseObject