《python解释器源码剖析》第1章--python对象初探
1.0 序
对象是python中最核心的一个概念,在python的世界中,一切都是对象,整数、字符串、甚至类型、整数类型、字符串类型,都是对象。换句话说,python中面向对象的理念观测的非常彻底,面向对象理论中的"类"和"对象"在python中都是通过某个对象实现的。
在python中,已经预先定义了一些类型对象,比如int类型、str类型、dict类型等,这些我们称之为内建类型对象,这些类型对象实现了面向对象中"类"的概念;这些内建对象实例化之后,可以创建类型对象所对应的实例对象,比如int对象、str对象、dict对象。这些实例对象可以视为面向对象理论中的"对象"这个概念在python中的体现。
同时python还允许程序员通过class A这样的表达式自己定义类型对象。基于这些类型对象,同样可以进行实例化的操作,创建的对象称之为实例对象。python中不光有着这些千差万别的对象,这些对象之间还存在着各种复杂的关系,从而构成了我们称之为类型系统
或对象体系
的东西
python中的对象体系是一个庞大而复杂的体系,不可能一下子解释清楚,因此我们的重点放在了了解对象在python内部是如何表示的。更确切的说,python是由C实现的,所以我们首先要弄清楚:对象
这个神奇的东西在C的层面是如何体现的,究竟是什么模样,是三头六臂、还是烈焰红唇、亦或是我太太蕾姆呢?
1.1 Python内的对象
从1989年Gudio在圣诞节揭开python的大幕开始,直到现在,python经历了一次又一次的升级,但是其实现语言一直都是C。我们知道,C并不是一个面向对象的语言,那么在python中,它的对象机制是如何实现的呢?
对于人的思维来说,对象是一个比较形象的概念,但对于计算机来说,对象却是一个抽象的概念。它并不能理解这是一个整数,那是一个字符串,计算机所知道的一切都是字节。通常的说法是,对象是数据以及基于这些数据的操作的集合。在计算机中,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也可能是离散的,这都不重要,重要的是这片内存在更高层次上可以作为一个整体来考虑,这个整体就是对象。在这片内存中,存储着一系列的数据以及可以对这些数据进行修改操作或读取操作的一系列代码。
在python中,对象就是C中的结构体在堆上申请的一块内存,一般来说,对象是不能被静态初始化的,并且也不能在栈空间上生存。唯一的例外就是类型对象,python中所有的内建的类型对象(如:整数类型对象、字符串类型对象),都是被静态初始化的
在python中,一个对象一旦被创建,它在内存中的大小就是不变的了。这就意味着那些需要容纳可变长度数据的对象只能在对象内维护一个指向一块可变大小的内存区域的指针。为什么要这样设计呢,因为遵循这样的规则可以使通过指针维护对象的工作变得非常简单。一旦允许对象的大小可在运行期改变,那么我们就可以考虑如下场景。在内存中有对象A,并且其后面紧跟着对象B。如果运行的某个时候,A的大小增大了,这就意味着必须将A整个移动到内存中的其他位置,否则A增大的部分会覆盖掉原本属于B的数据。只要将A移动到内存的其他位置,那么所有指向A的指针就必须立即得到更新。可想而知这样的工作是多么的繁琐。
1.1.1 对象机制的基石--PyObject
在python中,所有的东西都是对象,而所有的对象都拥有一些相同的内容,这些内容在PyObject中,PyObject是整个Python对象机制的核心.。至于PyObject是一个结构体,使用typedef定义。
object.h
/*
这个结构体是python对象机制的核心基石。
_PyObject_HEAD_EXTRA是一个在宏下面定义的,我们不用管,这东西仅仅是在debug下才有用。
*/
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
/*
ob_refcnt是关键,这是一个整形变量,与python的内存管理机制有关。
它表示一个对象(变量名就叫A吧)的引用计数,就是用多少个对象引用它
对于某一个对象,当有一个新的PyObject *引用的时候,其引用计数会增加。
而当这个PyObject *被删除时,则会减少。
当引用计数为0时,A指向的内存(对象)就会从堆上面被释放,从而供别的对象使用
ob_type是啥呢?我们看到这是一个指向_typeobject结构体的指针,那么这个结构体是什么呢?
实际上这个结构体对应着python内部的一种特殊对象,这是个用来指定对象类型的类型对象。
这个类型对象后面分析,现在我们发现python对象机制的核心非常简单,就是一个引用计数和类型信息。
*/
在这里多提一句,python中的变量实际上相当于一个便利贴,比如 a = 666,那么a这个变量就相当于一个便利贴。是先在内存中开辟一块空间,存上666这个值,然后让a这个变量指向它。像类似于go这种语言,必须要先声明。int a,相当于先在内存申请int大小的空间,命名为a,然后再往这个空间里面存值。而python则是像我们说的,是先有值,然后让变量指向这个值,所以python不存在先声明一个变量、然后给变量赋值这种情况,创建变量的时候就必须赋值。那么python是如何知道对象类型呢?根据值由解释器来推测类型,所以python创建变量的时候不需要显示的声明类型,并不代表没有类型,只不过python解释器根据值帮我们推测出了类型。因此有人说python没有类型(不需要声明类型),就是弱类型语言,都是在瞎说,记住:python是强类型语言,动态强类型语言。语言是强类型还是弱类型跟变量的声明方式没有关系。
那么如果改变a的值呢?对于go来说,这是直接将原来的内存里面的值清空掉,换上新的值,那么这就意味着前后的地址是不会改变的。而对于python来说,还是那句话python中的变量是一个便利贴,贴在对应的值上面,比如之前有一个a = 666,再来一个a = 777,那么对于go来说则是直接将666换成777,而对于python来说,则是在内存中新申请一份空间,然后空间里面存的值是777,然后让a指向这份内存,既然指向新的内存,就意味着python中,a的地址会发生改变。可能有人会问,那之前的666咋办?这就关系到刚才说引用计数了,既然a指向了777,是不是就说明666这份空间没有人指向了,那么引用计数是不是变成0了,所以就会被垃圾回收了。
代码实际演示
package main
import "fmt"
func main(){
var a = 666
fmt.Printf("%p\n", &a) // 0xc000062058
a = 777
fmt.Printf("%p\n", &a) // 0xc000062058
}
a = 666
print(id(a)) # 3187581417072
a = 777
print(id(a)) # 3187579733200
言归正传, 在PyObject中定义了每一个python对象都必须有的内容,这些内容将出现在每一个python对象所占有的内存的最开始的字节当中。这句话的另一个含义就是,每一个对象除了必须有这个PyObject之外,似乎还应该有一些其他的内存,来放置其它的东西。没错,倘若python中的对象只包含这个PyObject,那么python不是只有一种对象了。在PyObject中定义的内容仅仅是每一个python对象都必须有的一部分内容,至于其它的内容,则根据对象的不同而不同。
1.1.2 定长对象和变长对象
我们说每个对象除了有PyObject中的信息之外,还应该有一些其它的信息。那么整数对象(对于python2来讲,是PyIntObject)的其它信息就是C中的一个整型变量(但在python3中不是了,具体是什么,下一章介绍PyLongObject的时候会讲)。但是不幸的是,对于另一类对象就没有那么幸运了。如果你是python的设计师,你要如何实现字符串对象呢?很显然类似于PyLongObject对象,在PyObject之外,字符串对象应该维护一个字符串
,但在C中没有字符串的概念,所以准确的说,应该是字符串对象要维护n个char型变量
。这种对象实际上不光是字符串对象,比如对应于C++中list或vector的列表对象,它也应该维护n个PyObject对象
。看上去这种n个······
似乎也是一类python对象的共同特征,因此,python在PyObject对象之外,还有一个表示这类对象的结构体--PyVarObject
//object.h
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
/*
可以看到,PyVarObject是在PyObject的基础上,
又多了一个ob_size,Py_ssize_t类型看成int就好
*/
我们把整数对象这样不包含可变长度数据的对象称之为定长对象
,而字符串这样包含可变长度数据的对象称为变长对象
,它们的区别在于定长对象占用的内存大小是一样的,而变长对象占用的内存可能是不一样的。比如整数对象1和100
占用的内存大小都是sizeof(PyIntObject)
,而字符串对象所占用的内存大小就不一样了。正是这种区别导致了PyVarObject
对象中的ob_szie的出现。变长对象通常都是容器,ob_size这个成员实际上就是指明了变长对象中一共容纳了多少个元素。注意:ob_size指的是所容纳元素的个数,而不是指字节的数量。比如对于python中最常用的list。它就是一个PyVarObject
对象,如果某一时刻,这个list对象中有5个元素,那么它的ob_size就是5。
从定义就可以看出,PyVarObject
实际上只是对PyObject
的一个扩展。因此对于任何一个PyVarObject
,其所占用的内存,开始部分的字节的意义和PyObject
是一样的。换句话说,在Python的内部,每一个对象都拥有相同对象头部。这就使得在python中,对对象的引用变得非常统一,我们只需要用一个PyObject *
指针就可以引用任意的一个对象,而不论该对象实际是一个什么对象。
1.2 类型对象
在上面的描述中,我们看到了python中所有对象共有信息的定义。所以,当内存中存在某一个python对象时,该对象开始的几个字节的含义一定会符合我们的预想。但是当我们顺着时间轴回溯,就会发现一个问题:当在内存中创建对象,分配空间的时候,毫无疑问必须要知道申请多大的空间。显然这不会是一个定值,因为不同对象需要的空间不同,一个整数对象和一个字符串对象需要的空间是不同的。那么对象所需要的内存空间的大小信息到底在哪里呢?显然在PyObject
中没有这样的信息,其实这种说法也不对,因为这个信息虽然在PyObject
中看不到,但是却恰恰隐身于PyObject
中。
实际上,占用内存空间的大小是对象的一种元信息,这样的元信息是和对象所属类型密切相关的,因此它一定会出现在与对象所对应的类型对象当中。现在我们可以详细考察一下类型对象_typeobject
:
//object.h
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject; //_typeobject正是PyObject里面的一个成员
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name;
Py_ssize_t tp_basicsize, tp_itemsize;
destructor tp_dealloc;
printfunc tp_print;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
PyBufferProcs *tp_as_buffer;
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
traverseproc tp_traverse;
inquiry tp_clear;
richcmpfunc tp_richcompare;
Py_ssize_t tp_weaklistoffset;
getiterfunc tp_iter;
iternextfunc tp_iternext;
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
unsigned int tp_version_tag;
destructor tp_finalize;
#ifdef COUNT_ALLOCS
Py_ssize_t tp_allocs;
Py_ssize_t tp_frees;
Py_ssize_t tp_maxalloc;
struct _typeobject *tp_prev;
struct _typeobject *tp_next;
#endif
} PyTypeObject;
#endif
把部分注释去掉,长这个样子,在_typeobject
的定义中包含了很多信息,主要可以分为四类:
类型名:tp_name,主要是python内部以及调试的的时候使用
创建该类型对象时分配内存空间大小的信息,即tp_basicsize
和tp_itemsize
与该类型对象相关联的操作信息(就是诸如tp_print这样的许多的函数指针)
我们在下面将要描述的类型的类型信息
事实上,PyTypeObject
实例化(PyLong_Type->int,PyList_Type->list等等)
,就是python中面向对象理论的类
的具体实现,而PyTypeObject
也是一个非常复杂的话题,我们将在后面章节专门详细剖析构建在PyTypeObject
之上的python类型和对象体系。这里仅仅只是一个粗略的介绍,先有个印象即可,不会影响本节的学习。
1.2.1 对象的创建
考虑一下这个问题,假如我们命令python创建一个整数对象,python内部究竟如何才能从无到有地创建出一个整数对象呢?一般来说,python会有两种方法。第一种是通过python c api
来创建,第二种是通过类型对象PyLong_Type
。
python对外提供了c api,让用户可以从c环境中与python交互,实际上,python自己本身也是c写成的,所以python内部也大量使用了这些api。python中的c api分为两类,一类成为泛型的api,或者称之为AOL(abstract object layer)
。这类api具有PyObject_***
的形式,可以用于任何的python对象身上,比如输出对象的PyObject_Print
,你可以PyObject_Print(long object)
,也可以PyObject_Print(string object)
,api内部会有一整套机制确定最终调用的函数是哪一个。对于创建一个整数对象,我们可以采用如下的形式:PyObject* intObj = PyObject_New(PyObject, &PyLong_Type)
,注意:关于在python2.x源码中,int类型还不是PyLong_Type
,而是PyInt_Type
。同理int的实例对象在python2.x源码中也不是PyLongObject
,而是PyIntObject
,这也是2.x和3.x之间的一个区别。但是我们这里就不做区分了,写PyInt还是PyLong都无所谓,反正知道是一个东西就可以了。
另一类是与类型相关的api,或者称之为COL(concrete object layer)
。这类api通常只能作用在某一种类型的对象上,对于每一种内建对象,python都提供了这样的一组api。比如整数对象,我们可以使用如下的api创建,PyObject *intObj = PyLong_FromLong(10)
,这样就创建了一个值为10的对象。
不论采用哪一种C api,python内部最终都是直接分配内存,因为python对于内建对象是无所不知的。但是对于用户自定义的内省,比如通过class A(object):
,定义一个类型A,如果要创建A的实例对象(前面我们已经说过,实例对象可以视为面向对象理论中的对象
),python不可能实现就给我们提供PyObject_New(PyObject, &PyA_Type)
这样的 方式来创建。对于这种情况,python会通过A所对应的类型对象创建实例对象:
以上便是通过PyLong_Type
创建整数对象,实际上在python完成运行环境的初始化之后,符号int
就对应着一个类型为<type 'int'>
的对象(类对象或者类型对象),这个对象其实就是Python内部PyLong_Type
对象。当我们执行int(10)
这样的表达式时,就是通过PyLong_Type
创建了一个整数对象。
从图上我们还可以看到,int是一个继承自Object的类型(python2中存在经典类和新式类,python默认是全是新式类),类似于int对应python内部的PyLong_Type
,object则对应python内部的PyBaseObject_Type
。
我们用一张图来看看python创建整数对象的流程:
PyLong_Type(PyInt_Type)就是int这个类,在创建实例对象的时候,会先调用tp_new。这个tp_new就是python中类里面的new方法,如果tp_new为NULL(实际上肯定不会NULL,但是我们假设为NULL),那么会到tp_base指定的父类里面去寻找tp_new。在新式类当中,所有的类都继承自object,因此最终会找到一个不为NULL的tp_new。
然后通过tp_new会访问PyLong_Type中的tp_basicsize信息,继而完成申请内存的操作。这个信息记录着一个整数对象需要占用多大内存,在python源码中,你会看到这个值被设置成了sizeof(PyIntObject)
在调用type_new完成创建对象之后,流程就会转向PyLong_Type的tp_init,完成初始化对象的工作。
需要注意的是:这里只是以整数对象为例,说明类型对象在实例对象的创建过程中的作用,实际上从之后的分析会将会看到,作为python内建对象的整数对象在创建时是有一些不同的。
1.2.2 对象的行为
在PyTypeObject中定义了大量的函数指针,这些函数指针最终都会指向某个函数,或者指向NULL。这些函数指针可以视为类型对象中所定义的操作,而这些操作直接决定着一个类型对象在运行时所表现出的行为。比如int实例化对象、str实例化对象需要多少内存,这些都体现在PyTypeObject里面。PyTypeObject是一个结构体,int(PyLong_Type)
等其他类型,都是使用PyTypeObject结构体实例化得到的(PyTypeObject PyLong_Type = {...})
,只不过定义的类型对象不同,PyTypeObject成员的参数也不同,从而类型对象实例化得到的实例对象的属性也不同。
比如PyTypeObject中的tp_hash指明对于该类型的对象,如何生成其hash值。我们看到tp_hash是一个hashfunc类型的变量,在object.h当中,hashfunc实际是哪个是一个函数指针:typedef long (*hashfunc)(PyObject *)
。在上一节中我们还看到了tp_new、tp_init是如何决定一个实例对象被创建出来并初始化的。在PyTypeObject中指定不同的操作信息也正是一种对象区别于另一种对象的关键所在。
在这些操作信息中,有三组非常重要的操作族,在PyObject中,它们是tp_as_number、tp_as_sequence、tp_as_mapping。它们分别指向PyNumberMethods、PySequenceMethods、PyMappingMethods函数族,我们可以看看PyNumberMethods这个函数族:
//object.h
typedef struct {
/* Number implementations must check *both*
arguments for proper type and implement the necessary conversions
in the slot functions themselves. */
binaryfunc nb_add;
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
ternaryfunc nb_power;
unaryfunc nb_negative;
unaryfunc nb_positive;
unaryfunc nb_absolute;
inquiry nb_bool;
unaryfunc nb_invert;
binaryfunc nb_lshift;
binaryfunc nb_rshift;
binaryfunc nb_and;
binaryfunc nb_xor;
binaryfunc nb_or;
unaryfunc nb_int;
void *nb_reserved; /* the slot formerly known as nb_long */
unaryfunc nb_float;
binaryfunc nb_inplace_add;
binaryfunc nb_inplace_subtract;
binaryfunc nb_inplace_multiply;
binaryfunc nb_inplace_remainder;
ternaryfunc nb_inplace_power;
binaryfunc nb_inplace_lshift;
binaryfunc nb_inplace_rshift;
binaryfunc nb_inplace_and;
binaryfunc nb_inplace_xor;
binaryfunc nb_inplace_or;
binaryfunc nb_floor_divide;
binaryfunc nb_true_divide;
binaryfunc nb_inplace_floor_divide;
binaryfunc nb_inplace_true_divide;
unaryfunc nb_index;
binaryfunc nb_matrix_multiply;
binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;
你看到了什么,是的,这不就是python里面的魔法方法嘛。在PyNumberMethods里面定义了作为一个数值应该支持的操作。如果一个对象能被视为数值对象,比如整数,那么在其对应的类型对象PyIntType中,tp_as_number.nb_add就指定了对该对象进行加法操作时的具体行为。同样,PySequenceMethods和PyMappingMethods中分别定义了作为一个序列对象和关联对象(map),应该支持的行为,这两种对象的典型例子就是list和dict
然而对于一种类型来说,它完全可以同时定义三个函数中的所有操作。换句话说,一个对象既可以表现出数值对象的特征,也可以表现出关联对象的特征。
class Int(int):
def __getitem__(self, item):
return item
a = Int(1)
b = Int(2)
print(a + b) # 3
print(a["<{=....(嘎~嘎~嘎~)"]) # <{=....(嘎~嘎~嘎~)
看上去a[""]这种操作是一个类似于dict这样的对象才支持的操作。从int继承出来的Int自然是一个数值对象,但是通过重写__getitem__
这个魔法函数,可以视为指定了Int在python内部对应的PyTypeObject对象的tp_as_mapping.mp_subscript操作。最终Int实例对象表现的像一个map一样。归根结底就在于PyTypeObject中允许一种类型对象同时指定多种不同的行为特征。 默认使用PyTypeObject结构体实例化出来的PyLong_Type对象所生成的实例对象是不具备list和dict的属性特征的,但是我们继承PyLong_Type,同时指定__getitem__
,使得我们自己构建出来的类型对象所生成的实例对象,同时具备int、list(部分)、dict(部分)的属性特征,就是因为python支持同时指定多种行为特征。
1.2.3 类型的类型
我们之前说过,python里面一切皆对象。而在源码中,都是一个PyObject
结构体,这个PyObject
是一个最基本的结构体,包含了所有对象都有的共同属性,引用计数和类型
。不同的对象都是基于PyObject
再加上不同的属性所构建出来的新的结构体,所以说python中的对象本质上就是C语言中的malloc为结构体在堆上申请的一块内存
我们仔细观察PyTypeObject
,会发现第一个属性就是PyObject_Var_HEAD
,这个PyObject_Var_HEAD
,如果读了源码肯定不用我解释,但是如果没读,就再看我啰嗦一下吧。
//object.h
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject; // PyObject不用我说
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size;
} PyVarObject;
//这个也已经介绍了,就是在PyObject的基础上加了一个ob_size
//这个是专门针对变长对象的,ob_size表示变长对象中容纳了多少个元素
//l = [1, 2, 3, 4],显然l对应的ob_size就是4
#define PyObject_VAR_HEAD PyVarObject ob_base;
//这是一个宏定义,所以,这个PyObject_VAR_HEAD
//就等同于PyVarObject ob_base,说白了就是一个PyVarObject结构体实例
回到之前的问题,既然PyTypeObject
有PyVarObject
,说明有PyObject
,这就表明PyTypeObject
构建出来的、python中的int、dict、set之类的本身也是一个对象,这个之前已经解释过了,为了效果,大家还是配合我一下吧。喔~~~,天哪,这些类居然也是对象。是的,这种对象叫做类型对象,基于类型对象所构建出来的实例,称之为该类型对象的实例对象。在python中,任何一个东西都是对象,而每一个对象都是对应一种类型的,那么一个有趣的问题就来了,类型对象的类型是什么?不是说,python中一切皆对象吗?既然是对象,那么总要有一个类型实例化之后创建吧。是的没有错,这个创建类型对象的神秘之物就是PyType_Type
(对应python中的type,同理,这依旧是一个对象,依旧是通过PyTypeObject实例化得到,PyTypeObject PyType_Type ={}):
//typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
(destructor)type_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)type_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
(ternaryfunc)type_call, /* tp_call */
0, /* tp_str */
(getattrofunc)type_getattro, /* tp_getattro */
(setattrofunc)type_setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS, /* tp_flags */
type_doc, /* tp_doc */
(traverseproc)type_traverse, /* tp_traverse */
(inquiry)type_clear, /* tp_clear */
0, /* tp_richcompare */
offsetof(PyTypeObject, tp_weaklist), /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
type_methods, /* tp_methods */
type_members, /* tp_members */
type_getsets, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyTypeObject, tp_dict), /* tp_dictoffset */
type_init, /* tp_init */
0, /* tp_alloc */
type_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
(inquiry)type_is_gc, /* tp_is_gc */
};
PyType_Type在python的类型机制当中是一个至关重要的对象,所有内建的类:int、dict、甚至object、以及用户自定义class都是基于PyType_Type创建的。
class A:
...
print(A.__class__)
print(int.__class__)
print(type.__class__)
"""
<class 'type'>
<class 'type'>
<class 'type'>
"""
输出的<class 'type'>
就是python内部的PyType_Type
,它是所有class的class,在python中被称为metaclass(元类)
,我们注意到type的class还是type,一切皆对象,type也是对象,至于生成type的对象则就是type自己本身,所以type把自己都变成了自己实例化对象,连自己都不放过,就更不用说其他的class了。所以type是一个古老但却又及其强大的类,说它古老是因为学python没多久就用过,说它强大是因为我们目前只见识到它的冰山一角,我们使用它可以玩出很多高级的花样。关于PyType_Type和metaclass
,我们在后续会详细阐述,这就不多说了。
我们下面看看PyLongType是如何与PyType_Type建立关系的。前面提到,每个对象都有自己的引用计数、类型信息保存在开始的部分(PyObject)中。为了方便对这部分内存的初始化,python中提供了几个有用的宏。
//object.h
#ifdef Py_TRACE_REFS //如果有Py_TRACE_REFS这个宏
/* 定义指针,维护所有堆区对象的双链表 */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
/* 定义宏_PyObject_EXTRA_INIT,代替0, 0, */
#define _PyObject_EXTRA_INIT 0, 0,
#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif
/* PyObject_HEAD 定义了每一个PyObject的初始段 */
#define PyObject_HEAD PyObject ob_base;
/*
PyObject_HEAD_INIT(type),也是一个宏
只不过可以看成是带有参数的宏,至于下面的{}
是因为宏不仅能替代一行,还可以替代多行,用{}包起来
最后的\则是对\n进行转义
*/
#define PyObject_HEAD_INIT(type) \
{ _PyObject_EXTRA_INIT \
1, type },
/*
回顾PyObject和PyVarObject定义,初始化的动作就很清晰了,实际上这些宏在各种内建类型对象的初始化中被大量的使用着。以PyLong_Type为例,可以更清晰的看到一般的类型对象和这个特例独行的PyType_Type之间的关系:
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
long_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
long_to_decimal_string, /* tp_repr */
&long_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)long_hash, /* tp_hash */
0, /* tp_call */
long_to_decimal_string, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_LONG_SUBCLASS, /* tp_flags */
long_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
long_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
long_methods, /* tp_methods */
0, /* tp_members */
long_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
long_new, /* tp_new */
PyObject_Del, /* tp_free */
};
目前出现了很多概念,我们来梳理一下。凡是Py***_Type
这种形式(如PyLong_Type、PyList_Type、PyDict_Type、PyType_Type等等)
,都是由结构体PyTypeObject
实例化得到的,在python里面就是int、list、dict、type这种类型对象。
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int",
...
}
PyTypeObject PyList_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list",
sizeof(PyListObject)
...
}
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type",
...
}
像PyLongObject、PyListObject、PyDictObject等等则对应python中的int实例对象、list实例对象、dict实例对象。可能有人觉得比较迷,我再来解释一下,先来看看这些对象的定义
//int
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
typedef struct _longobject PyLongObject;
//list
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
//dict
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
uint64_t ma_version_tag;
PyDictKeysObject *ma_keys;
} PyDictObject;
//set
typedef struct {
PyObject_HEAD
Py_ssize_t fill; /* Number active and dummy entries*/
Py_ssize_t used; /* Number active entries */
Py_ssize_t mask;
setentry *table;
Py_hash_t hash; /* Only used by frozenset objects */
Py_ssize_t finger; /* Search finger for pop() */
setentry smalltable[PySet_MINSIZE];
PyObject *weakreflist;
} PySetObject;
我们注意到这些结构体实例化之后,就对应着python中的实例对象。而类型是这个结构里面的一个属性,PyTypeObject(隐藏在PyObject里面),可能有人惊讶了,类型居然在值里面。所以说python是先有值,当我们写a = 111
的时候,我们没有告诉python这是int
,而是python解释器通过值来推断出这是个int
,从而知道这对应的是PyLongObject
结构体,于是直接将值设置到结构体当中,然后再指定PyTypeObject(类型)
。
在这里我再提一个很多人都会困惑的问题,就是type和object之间的关系
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(type)) # <class 'type'>
# 相信上面都很好理解
# 但是下面就有点让人费解了
# object是由type创建,但是与此同时却又继承自object
# 因此就类似先有鸡还是先有蛋的问题
# 先有object,object是由type生成的,
# 先有type,type又继承object。
# 两者刚好形成一个闭环
print(type(object)) # <class 'type'>
print(type.__base__) # <class 'object'>
在初学python的时候,都知道object
是所有类的顶层父类,type
创建了所有类。说明object
是type
的父类,object
又是type
创建的对象,那这不就矛盾了吗?就像代码中说的那样,到底是先有哪一个呢?那么下面就用我们上面学到的知识解释一下。
首先,python中一切皆对象,不论是类对象,还是实例对象,都是对象。而python中的对象本质上就是c语言中的malloc函数为结构体在堆上申请的一块内存。类对象在c层面都是PyTypeObject,比如int对应PyLong_Type、list对应PyList_Type、object对应PyBaseObject_Type,实例对象则是PyLongObject、PyListObject、PyDictObject、PySetObject····等等。而且每一个结构体都有一个ob_type,PyLongObject的ob_type对应PyLong_Type、PyListObject对应PyList_Type等等,而对于PyLong_Type、PyList_Type、PyBaseObject_Type等等,它们的ob_type统统都指向了PyType_Type(python中的type),所以我们说type是生成了所有类,也就是元类。并且每一个PyXXX_Type内部还有一个tp_base属性,表示继承的父类,这一点我们在后面第十三章剖析python中的类机制的时候会详细介绍,目前只需要知道这个tp_base都指向了PyBaseObject_Type,所以说python中所有的类都继承自object。至于为什么这么做呢?因为我们需要能够通过反射等机制获取这些类对象的属性,并且还可以在运行时对其进行修改。这就意味着必须有一个对象实例化值能够保存类对象的属性信息,或者内部有指针在指向这些类对象,在运行时可以动态对类对象进行属性上的添加、修改、删除等等,这个对象就是type,正是因为有了type,python才会变得如此动态。像int、str、list这些内建对象、包括我们自己自定义的类创建都需要经过type,由type控制生成类的这一过程。
我们说python所有对象在底层都是一个嵌套了PyObject的结构体,都会有一个类型,Py···Object的类型则对应相应的Py···_Type,而Py···Type的类型则全部对应PyType_Type。Py···Type都是由PyTypeObject生成的,PyTypeObject嵌套在Py···Object里面,
type(123)
相当于从PyLongObject
中拿到对应的类型(PyTypeObject->PyLong_Type)
,type(type(123))
则是从PyLongObject
中的PyTypeObject
拿到对应的类型(PyType_Type
)。
1.3 python对象的多态性
通过PyObject
和PyTypeObject
,python利用C语言完成了C++所提供的对象的多态的特性。在python创建一个对象,比如PyLongObject
时,会分配内存,进行初始化。然后python内部会用一个PyObject *
变量(注意:不是PyTypeObject *)
来保存和维护这个对象。其他对象也是与此类似,所以在python内部各个函数之间传递的都是一种泛型指针:PyObject *
。这个指针所指向的对象是什么类型的,我们并不知道,只能从指针所指向的ob_type域
进行动态判断,而正是通过ob_type这个域
,python实现了多态机制。
考虑下面的Print函数:
void Print(PyObject * object)
{
object -> ob_type -> tp_print(object);
}
如果传给Print函数的指针是一个PyLongObject*
,那么它就会调用PyLongObject
对象对应的类型对象(PyLong_Type)
中定义的输出操作。如果指针是一个PyListObject*
,那么就会调用PyListObject
对象对应的类型对象(PyList_Type)
中定义的输出操作。可以看到,这里同一个函数在不同情况下表现出了不同的行为,这正是多态的核心所在。
前面提到的AOL
的C api正式建立在这种多态机制上面的,比如下面的例子:
//object.c
long PyObject_Hash(PyObject *v)
PyTypeObject *tp = v -> ob_type;
if (tp -> tp_hash != NULL)
{
return (*tp -> tp_hash)(v)
}
1.4 引用计数
在c和c++中,程序员被赋予了极大的自由,可以任意的申请内存。但是权利的另一面对应着责任,程序员最后不使用的时候,必须负责将申请的内存释放,并释放无效指针。可以说,这一点是万恶之源,大量内存泄漏和悬空指针的bug由此产生
现代的开发语言当中都有垃圾回收机制,语言本身负责内存的管理和维护,比如java和c#。垃圾回收机制将开发人员从维护内存分配和清理的繁重工作中解放出来,但同时也剥夺了程序员和内存亲密接触的机会,并牺牲了一定的运行效率。但好处就是提高了开发效率,并降低了bug发生的几率。python里面同样具有垃圾回收机制,代替程序员进行繁重的内存管理工作,而引用计数正是垃圾收集机制的一部分。
python通过对一个对象的引用计数的管理来维护对象在内存中的存在与否。我们知道python中每一个东西都是一个对象,都有一个ob_refcnt
变量。这个变量维护这该对象的引用计数,从而也最终决定着该对象的创建与消亡。
在python中,主要是通过Py_INCREF(op)
和Py_DECREF(op)
两个宏,来增加和减少一个对象的引用计数,当一个对象的引用计数减少到0后,Py_DECREF
将调用该对象的析构函数来释放该对象所占有的内存和系统资源。这个析构函数就是对象的类型对象(Py***_Type)
中定义的函数指针来指定的,就是那个tp_dealloc
.
如果熟悉设计模式中的Observer
模式,就可以看到,这里隐隐约约透着Observer
模式的影子。在ob_refcnt
减少到0时,将触发对象的销毁事件。从python的对象体系来看,各个对象提供了不同事件处理函数,而事件的注册动作正是在各个对象对应的类型对象中完成的
PyObject
中的ob_refcnt
是一个32位的整形变量,这隐含着python中,对任何一个变量的引用都不能超过这个整形变量的最大值。可以肯定的说,这个条件基本上是百分之百成立,只要不是吃饱了撑的,写恶意代码。
python里面一切皆对象,类型对象也是对象,那么它们的引用计数是多少呢?需要注意的是:类型对象是超越引用计数规则的,永远都不会被析构。每一个指向类型对象的指针不能视为对类型对象的引用。a = 123,123是一个int,但是这并不表示int的ob_refcnt就加1了
在python的源代码中也可以看到,在不同的编译选项下(Py_REF_DEBUG,Py_TRACE_REFS,这些都是debug、追踪时候用的。)
,引用计数的宏还要做很多其它的额外工作。
#define _Py_NewReference(op) ( \
_Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
Py_REFCNT(op) = 1)//对于新创建的对象,引用计数为1
#define _Py_ForgetReference(op) _Py_INC_TPFREES(op)
#ifdef Py_LIMITED_API
PyAPI_FUNC(void) _Py_Dealloc(PyObject *);
#else
#define _Py_Dealloc(op) ( \
_Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA \
(*Py_TYPE(op)->tp_dealloc)((PyObject *)(op)))//析构函数
#endif
#endif /* !Py_TRACE_REFS */
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++) //引用计数增加
#define Py_DECREF(op) \
do { \
PyObject *_py_decref_tmp = (PyObject *)(op); \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--(_py_decref_tmp)->ob_refcnt != 0) \
_Py_CHECK_REFCNT(_py_decref_tmp) \
else \
_Py_Dealloc(_py_decref_tmp); \
//如果引用计数为0,执行析构函数
} while (0)
#define Py_CLEAR(op) \
do { \
PyObject *_py_tmp = (PyObject *)(op); \
if (_py_tmp != NULL) { \
(op) = NULL; \
Py_DECREF(_py_tmp); \
} \
} while (0)
//两个宏,处理对象指针为NULL的情况
#define Py_XINCREF(op) \
do { \
PyObject *_py_xincref_tmp = (PyObject *)(op); \
if (_py_xincref_tmp != NULL) \
Py_INCREF(_py_xincref_tmp); \
} while (0)
#define Py_XDECREF(op) \
do { \
PyObject *_py_xdecref_tmp = (PyObject *)(op); \
if (_py_xdecref_tmp != NULL) \
Py_DECREF(_py_xdecref_tmp); \
} while (0)
在一个对象的引用计数为0时,与该对象对应的析构函数就会被调用,但是要特别注意的是,调用析构函数并不意味着最终一定调用free
释放空间。如果真是这样的话,那么频繁申请、释放内存空间会使python的执行效率大打折扣(更何况python已经背负了人们对其执行效率的不满这么多年)。一般来说,python中大量采用了内存对象池的技术,使用这种技术可以避免频繁地申请和释放内存空间。因此在析构的时候,只是将对象占用的空间归还到内存池中。python在操作系统之上提供了一个内存池,说白了就是对malloc进行了一层封装,实现申请一部分内存,然后用于对象的创建,这样就不必频繁地向操作系统请求空间了,从而大大的节省时间。这一点,在后面的python内建对象(PyLongObject,PyListObject等等)
的实现中,将会看得一清二楚。
1.5 python对象的分类
根据功能和特征:我们可以把python中的对象大致分为以下几类:
Fundamental对象:类型对象,如type
Numeric对象:数值对象,如int、float、bool
Sequence对象:序列对象,如str、list、tuple
Mapping对象:关联对象(映射对象),如dict
Internal对象:python虚拟机在运行时内部使用的对象,如function、code、frame、module、method
《python解释器源码剖析》第1章--python对象初探的更多相关文章
- 《python解释器源码剖析》第13章--python虚拟机中的类机制
13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...
- 《python解释器源码剖析》第12章--python虚拟机中的函数机制
12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...
- 《python解释器源码剖析》第9章--python虚拟机框架
9.0 序 下面我们就来剖析python运行字节码的原理,我们知道python虚拟机是python的核心,在源代码被编译成字节码序列之后,就将有python的虚拟机接手整个工作.python虚拟机会从 ...
- 《python解释器源码剖析》第0章--python的架构与编译python
本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...
- 《python解释器源码剖析》第11章--python虚拟机中的控制流
11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...
- 《python解释器源码剖析》第8章--python的字节码与pyc文件
8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...
- 《python解释器源码剖析》第7章--python中的set对象
7.0 序 集合和字典一样,都是性能非常高效的数据结构,性能高效的原因就在于底层使用了哈希表.因此集合和字典的原理本质上是一样的,都是把值映射成索引,通过索引去查找. 7.1 PySetObject ...
- 《python解释器源码剖析》第4章--python中的list对象
4.0 序 python中的list对象,底层对应的则是PyListObject.如果你熟悉C++,那么会很容易和C++中的list联系起来.但实际上,这个C++中的list大相径庭,反而和STL中的 ...
- 《python解释器源码剖析》第2章--python中的int对象
2.0 序 在所有的python内建对象中,整数对象是最简单的对象.从对python对象机制的剖析来看,整数对象是一个非常好的切入点.那么下面就开始剖析整数对象的实现机制 2.1 初识PyLongOb ...
随机推荐
- LVS系列一、LVS集群-NAT模式
一. 集群概述 1. 什么是集群? 一组各自相互独立且又相互依赖的,通过高速网络互联的计算机组成的一个计算机组, 以单一的系统模式加以管理, 为用户提供服务, 对用户来说, 用户只会认为对方是一个服务 ...
- wxPython在frame窗口修改图标
self.m_panel4 = wx.Panel( self.m_notebook5, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TR ...
- spring 配置参数从配置文件中加载到PropertiesFactoryBean 和配置参数从数据库加载到PropertiesFactoryBean 的实现,及项目中的相关应用
1.加载.properties文件中的配置参数加载到PropertiesFactoryBean容器中 <bean id="configProperties" class=&q ...
- python3使用tkinter之Menu坑
添加菜单之后,下拉菜单的第一行是一条虚线,点击会在窗口的左上角独立显示下拉菜单,如下图所示: 去掉的方法是:创建文件菜单的时候,添加 tearoff=0参数 tearoff 有 0 和 1 两个值,分 ...
- DRF视图-基类
2个视图基类 REST framework 提供了众多的通用视图基类与扩展类,以简化视图的编写. 为了区分上面请求和响应的代码,我们再次创建一个新的子应用: python manage.py star ...
- IntelliJ IDEA 2019.2.1 破解教程, 最新激活码(激活到2089年8月,亲测有效,持续更新中...)
当前最新版本 IDEA 2019.2.1 本来笔者这边是有个正版激活码可以使用的,但是,2019.9月3号的时候,一些小伙伴反映这个注册码已经失效了,于是拿着自己的 IDEA, 赶快测试了一下,果不其 ...
- java面试指导2019-9-10
11. Java 面向对象编程三大特性: 封装 继承 多态 封装 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问.但是如果一 ...
- [转帖]字符编码笔记:ASCII,Unicode 和 UTF-8
字符编码笔记:ASCII,Unicode 和 UTF-8 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html 转帖 ...
- mount.nfs: access denied by server while mounting
在利用centos7系统搭建NFS服务时出现如下问题,百度后才解决 因为当时在服务器端vim /etc/exports 时, 我只写了 这一行 /home/wjs-nfs *(ro) (没想到偷懒出 ...
- WPF——Application
Application类处于WPF应用程序的最顶端,main函数就在这个类中. Application类的作用: 截图连接 https://docs.microsoft.com/zh-cn/dotne ...