第1篇:CPython实现原理:万物皆为PyObject

对象的定义

在C/C++中,对象就是堆(Heap)内存中的内存实体,从简单的基本数据类型(int,float,char)到聚合的数据类型(struct)一切皆为对象,我们说基本的数据类型是简单的对象(Simple Object),因为它仅包含数据属性。而struct级别的数据类型是完整的对象(Concrete Object),因为完整的对象具有属性行为两个基本概念。

  • 属性就是结构体的数据字段,可以是基本数据类型,指针类型、甚至是嵌套的struct类型。
  • 行为又称为方法成员函数,就是struct内部定义的一系列函数指针。

备注:如有疑问:请移至《C++ 面向对象》《C++ 多态》

Python对象的本源 PyObject

CPython是用C语言实现的,那么用C/C++中关于对象的概念,去理解Python对象也是理所当然的。先看一下CPython中关于PyObject的定义

typedef struct _object {
    _PyObject_HEAD_EXTRA  
    Py_ssize_t ob_refcnt;     //引用计数器,和内存回收有关
    PyTypeObject *ob_type;  //定义Python对象的元类信息
} PyObject;

其实整个PyObject的难点是就是第三个字段PyTyepObject,也是整个PyObject的核心,包括基本的类型信息:类名称,类型尺寸(需要分配多大的内存)以及类绑定的方法(即绑定的函数指针)。后文会详细谈到,而_PyObject_HEAD_EXTRA这个宏的定义如下

#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

#define _PyObject_EXTRA_INIT 0, 0,

#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif

而以发布模型的编译CPython源代码的话,_PyObject_HEAD_EXTRA这段宏定义是不存在,因此PyObject的定义可以简化为

typedef struct _object {
    Py_ssize_t ob_refcnt;     //引用计数器,和内存回收有关
    PyTypeObject *ob_type;  //定义Python对象的元类信息
} PyObject;

在Python的世界观中一切皆为PyObject这个话怎么理解呢?在Python语义中,每当我们实例化任意一个Python对象,在其占用的堆内存区块的首个字节就包含一个PyObject定义的副本,除PyObject的相关内存字节副本外,跟随PyObject对象内存副本之后的内存字节是当前对象的内存信息。举个例子,比如PyLongObject,继承PyVarObject,我们先看看位于Include/longintrepr.h定义

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

而PyObject_VAR_HEAD这个宏定义实质上是PyVarObject类型,而PyVarObject本来就继承PyObject,其定义如下Include/object.h

....
#define PyObject_VAR_HEAD      PyVarObject ob_base;
....
typedef struct {
    PyObject ob_base;
    //这是一个指向C类型的数据指针指向的堆内存区域
    //ob_size就是统计该堆内存能够容纳元素个数的计数器
    Py_ssize_t ob_size; 
} PyVarObject;

任何一个Python对象继承PyVarObject,表明它是一个可变长对象(或叫容器对象),其PyLongObject的完整形式的定义如下

struct _longobject{
    PyObject ob_base;   //PyObject的内存副本
    Py_ssize_t ob_size; //数据指针的计数器
    //其实ob_digit是一个堆中的数组,只不过目前指向索引1的元素
    digit ob_digit[1];        
} PyLongObject;

或者是这样

struct _longobject{
    Py_ssize_t ob_refcnt;     //引用计数器,和内存回收有关
    PyTypeObject *ob_type;    //定义Python对象的元类信息
    Py_ssize_t ob_size;       //数据指针的计数器
   //数据指针 ob_digit,而ob_digit[1]标识一个digit类型的数组元素
    digit ob_digit[1];         
} PyLongObject;

好吧!用一个类继承图就比较直观


从内存图的角度来看,如下图所示,PyLongObject类型的对象,我们说当实例化一个PyLongObject的实例,它首先要初始化PyVarObject的内存中的数据(所有字段的默认值),我们说PyLongObject的实例持有PyVarObject的内存副本。而PyVarObject初始化,也要初始化PyObject的内存数据。换句话说,在Python内部,每个堆吸纳个都拥有相同的对象头部,这使对象的引用变得单一化,只需一个PyObject*指针就可以任意引用一个对象


备注:其实C++面向对象模型大体上是这么一个套路,只不过C++运行时增加了一些对象访问控制设定,而C实现的PyObject是不存在所谓访问控制设定一回事。

定长对象和变长对象

在CPython 3.x像整数类型的Python对象,其具体的数字字面量ob_digit[1](注意ob_digit是一个unsigned int数组)。例如

>>> n=int(1)
>>> n2=int(9999)
>>> n
1
>>> n2
9999
>>> import sys
>>> sys.getsizeof(n)
28
>>> sys.getsizeof(n2)
28

我们说像int,double这类的基本数据类型,他们不同的实例有固定长度的,我们说这些叫定长对象。而变长对象即对象的类型尺寸是可变的。例如PyStringObject、PyListObject、PyDictObject这些都是可变长对象(也叫容器对象,这和C++标准库的容器对象非常相似了)。容器对象的最基本的特征。其struct内部维护着一个数据指针(指向堆中一片连续的内存区域),以及一个计数器ob_size就是实时统计该堆内存区域有多少个数据实体。目前仅需简单了解这些概念即可。

PyTypeObject

我们说过任意一个PyObject的实例创建过程中,需要知道类型名,需要分配的堆内存,以及该对象实例配套的行为(即函数指针),这些信息均包含在PyTypeObject实例中,注意我的描述,这个关键字定义必须牢记。

重要概念: PyTypeObject实例是指在CPython3.x源码中,满足“Py<类型名称>_Type”这样的命名风格的结构体初始化代码都叫PyTypeObject实例。

例如PyLongObject,对应的PyTypeObject实例就是PyLong_Type,PyListObject对应的PyTypeObject实例是PyList_Type,等等。下文会详细谈到。

我们先查看一下PyTypeObject的类定义,如下所示,完整代码见Include/cpython/object.h的第193行-274行

struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; //类型名称
    //内存分配的类型尺寸
    Py_ssize_t tp_basicsize, tp_itemsize; 
    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    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;

    //函数来访问对象作为I/O缓冲器
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */
    ...
    //属性描述符和子类相关信息,这里定义
    //这里定义了对象的行为
    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;
    .....
}  PyTypeObject;

在PyTypeObject中有三个非常重要的字段,分别是tp_as_number、tp_as_sequence、tp_as_mapping。它们分别指向PyNumberMethod、PySequenceMethods和PyMappingMethods的函数族。

  • PyNumberMethod:定义了Python对象的行为可以像数字类型执行乘除加减等操作。
  • PySequenceMethods:定义了Python对象作为一个顺序表一样的行为,例如list。
  • PyMappingMethods:定义Python对象的关联行为,例如dict

Python对象的类型信息

从PyTypeObject定义中起始字段是一个PyObject_VAR_HEAD,这说明PyTypeObject也是PyObject,正好说明CPython中,一切事物都是Python对象,而每个对象有其一个对应的Type。注意:**不论什么编程语言,当说一个对象是什么类型,即意味最起码的三点信息:1.类型名称、2.类型尺寸、3.对象内存地址。

来看一个例子来展示一下什么是PyTypeObject实例

>>> class Student(object):
...     pass
... 
>>> type(Student)
<class 'type'>
>>> st=Student()
>>> type(st)
<class '__main__.Student'>
>>> i=int(10)
>>> type(i)
<class 'int'>
>>> type(int)
<class 'type'>
>>> 

每个用户定义的Python对象的实例可以由其类内部关联的PyTypeObject确定其类型(即type名称、其对象占用堆内存尺寸、对象内存地址...),那么PyTypeObject本身也是Python对象,其类型是什么呢?就是Type类型,当然PyTypeObject的类型由PyType_Type实例来确定。具体的代码见详细代码请查阅Objects/typeobject.c第3738行-3781行

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 */
    offsetof(PyTypeObject, tp_vectorcall),      /* tp_vectorcall_offset */
    ....
    (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 |
    Py_TPFLAGS_HAVE_VECTORCALL,                 /* 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 */
    ....
};

这里需要思考一个问题,当我们在解释器接受到类似type(Student)、type(123)这些语句,CPython内部如何获取其类型信息呢?我们之前说过任何一个Python对象内存头部保存着PyObject的内存副本,更明确地说是引用计数器PyTypeObject内存副本在Python中所有class关键字的类定义都通过一个与其对应的PyTypeObject实例来创建该类型的对象。比如Python的int类型对应C层面的PyLongObject类,而PyLongObject的实例化由对应的PyLong_Type实例提供类型信息。如此类推,还有其他常见的Python对象与PyTypeObject实例的对应关系,如下表所示

在CPython的运行时中,所有内置的数据类型已经在C代码中预定义了所有对应类型的PyType_Type实例了,并且这些实例仅在运行时初始化一次

这里我们仍然以PyLongObject为例, 当一个我们实例化一个int类型的对象时,CPython内部需要引用对应的PyLong_Type实例内的类型信息。PyLong_Type实例的初始化语句见具体源代码,见Objects/longobject.c的第5671行-5712行,从PyLong_Type源码可知PyVarObject_HEAD_INIT(&PyType_Type,0)宏定义在编译时已经经历过如下的替换过程。

我们说的宏表达式PyVarObject_HEAD_INIT(&PyType_Type)最终等价于

{{0,0,1, &PyType_Type },0},

那么PyLong_Type实例的最终代码形式,如下所示,

PyTypeObject PyLong_Type = {
    {{0,0,1, &PyType_Type },0},
    "int",                                      /* tp_name */
    offsetof(PyLongObject, ob_digit),           /* tp_basicsize */
    sizeof(digit),                              /* tp_itemsize */
    ....
    long_to_decimal_string,                     /* tp_repr */
    &long_as_number,                            /* tp_as_number */
    ....
    (hashfunc)long_hash,                        /* tp_hash */
    ....
    PyObject_GenericGetAttr,                    /* tp_getattro */
    ....
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
        Py_TPFLAGS_LONG_SUBCLASS,               /* tp_flags */
    long_doc,                                   /* tp_doc */
    ....
    long_richcompare,                           /* tp_richcompare */
    ....
    long_methods,                               /* tp_methods */
    ....
    long_getset,                                /* tp_getset */
    ....
    long_new,                                   /* tp_new */
    PyObject_Del,                               /* tp_free */
};

如果,你上面代码还没有概念的话,我们通过一个实例来说明一些基本的事实,首先我们在Python解释器交互命令行创建一个比较无聊的num函数,该函数旨在其内部实例化一个PyLongObject的实例m

可以用gdb工具执行Python解释器,我们通过对Python/ceval.c源码关于LOAD_CONST指令定义的C代码的执行断点,

  • 使用print *value命令,可以知道初始化一个PyLongObject实例时,它的ob_type字段是指向PyLong_Type实例的内存地址。
  • 通过print一下*(value->ob_type),我们得到PyLong_Type实例的所有具体细节,该PyLong_Type实例的ob_type字段指向一个PyType_Type实例的内存地址,并且tp_base字段是指向一个PyBaseObject_Type的实例。


药不要停,如此类推我们通过print打印ob_base找到当前PyType_Type子类实例的本体,通过ob_type找到上一级实例的

显然,根据PyVarObject_HEAD_INIT(&PyType_Type,0)可知,在PyLongObject实例化过程中,会引用PyLong_Type实例,而PyLong_Type实例也会引用PyType_Type实例。而PyType_Type实例是整个CPython类型系统的根,也就是说PyType_Type是所有Python对象的元类。我们可以通过一个内存图来得到一个清晰的轮廓

咦,这不就是类似C++类继承的原理吗?不用猜。就是,同时这也是体现CPython面向对象模式中的另一个特征--多态。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277