Python源码剖析[2] —— 对象机制

[绝对原创 转载请注明出处]

Python源码剖析

——对象机制

本文作者: Robert Chen(search.pythoner@gmail.com)

1.      对象

Python的世界中,一切都是对象,一个整数是一个对象,一个字符串也是一个对象,更为奇妙的是,类型也是一个对象,整数类型是一个对象,字符串类型也是一个对象。从1980Guido在那个圣诞节揭开Python世界的大幕开始,一直到现在,Python经历了一次一次的升级,但是其实现语言一直都是ANSI C。我们知道,C并不是一个面向对象的语言,那么在Python中,它的对象机制是如何实现的呢?

对于人的思维来说,对象是一个比较形象的概念,而对于计算机来说,对象实际上是一个抽象的概念。计算机并不能理解这是一个整数,那是一个字符串,对于计算机来说,它所知道的一切都是字节。通常的说法是,对象是数据以及基于这些数据的操作的集合。在计算机上,一个对象实际上就是一片被分配的内存空间,这些内存可能是连续的,也有可能是离散的,这都不重要,重要的是这片内存在更高的层次上可以作为一个整体来考虑,这个整体就是一个对象。在这片内存中,存储着一系列的数据以及可以对这些数据进行修改或读取的一系列操作的代码。

Python中,对象就是在堆上申请的结构体,对象不能是被静态初始化的,并且也不能是在栈空间上生存的。唯一的例外就是类型对象(type object)Python中所有的类型对象都是被静态初始化的。

Python中,一个对象一旦被创建,它在内存中的大小就是不变的了。这就意味着那些需要容纳可变长度数据的对象只能在对象内维护一个指向一个可变大小的内存区域的指针。为什么要设定这样一条特殊的规则呢,因为遵循这样的规则可以使通过指针维护对象的工作变得非常的简单。因为一旦允许对象的大小可在运行期改变,我们可以考虑如下的情形。在内存中有对象A,并且其后紧跟着对象B。如果运行期某个时刻,A的大小增大了,这意味着必须将整个A移动到内存中的其他位置,否则A增大的部分将覆盖原本属于B的数据。一旦将A移动到内存中的其他位置,那么所有指向A的指针必须立即得到更新,光是想一想,就知道这样的工作是多么的恐怖。

Python中,所有的东西都是对象,而所有的对象都拥有一些相同的内容,这些内容在PyObject中定义,PyObject是整个Python对象机制的核心。

[object.h]

typedef struct _object {

    PyObject_HEAD

} PyObject;

 

 

 

实际上,PyObjectPython中不包含可变长度数据的对象的基石,而对于包含可变长度数据的对象,它的基石是PyVarObject

[object.h]

typedef struct {

    PyObject_VAR_HEAD

} PyVarObject;

 

 

 

这两个结构体构成了Python对象机制的核心基石,从代码中我们可以看到,Python的对象的秘密都隐藏在PyObject_HEADPyObject_VAR_HEAD中。

[object.h]

#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

 

/* PyObject_HEAD defines the initial segment of every PyObject. */

#define PyObject_HEAD           /

    _PyObject_HEAD_EXTRA        /

    int ob_refcnt;          /

    struct _typeobject *ob_type;

 

#define PyObject_VAR_HEAD       /

    PyObject_HEAD           /

    int ob_size; /* Number of items in variable part */

 

 

 

PyObject_HEAD中定义了每一个Python对象都必须有的内容,这些内容将出现在每一个Python对象所占有的内存的最开始的字节中,从PyObject_VAR_HEAD的定义可以看出,即使对于拥有可变大小数据的对象,其最开始的字节也含有相同的内容,这就是说,在Python中,每一个对象都拥有相同的对象头部。这就使得在Python中,对对象的引用变得非常的统一,我们只需要用一个PyObject *就可以引用任意的一个对象,而不论该对象实际是一个什么对象。

PyObject_HEAD的定义中,我们注意到有一个ob_refcnt的整形变量,这个变量的作用是实现引用计数机制。对于某一个对象A,当有一个新的PyObject *引用该对象时,A的引用计数应该增加;而当这个PyObject *被删除时,A的引用计数应该减少。当A的引用计数减少到0时,A就可以从堆上被删除,以释放出内存供别的对象使用。

PyObject_HEAD中,我们注意到ob_type是一个指向_typeobject结构体的指针,那么这个结构体是一个什么东西呢?实际上这个结构体也是一个对象,它是用来指定一个对象类型的类型对象。这个类型对象我们将在后边详细地考察。现在我们看到了,在Python中实际上对象机制的核心非常的简单,一个是引用计数,一个就是类型。

而对于拥有可变长度数据的对象,这样的对象通常都是容器,我们可以在PyObject_VAR_HEAD中看到ob_size这个变量,这个变量实际上就是指明了该对象中一共包含了多少个元素。注意,ob_size指明的是元素的个数,而不是字节的数目。比如对于Python中最常用的list,它就是一个PyVarObject对象,如果某一时刻,这个list中有5个元素,那么PyVarObject.ob_size的值就是5

2.      类型对象

在上面的描述中,我们看到了Python中所有对象的对象头的定义。所以,当内存中存在某一个Python的对象时,该对象的开始的几个字节的含义一定会符合我们的预期。但是,当我们把眼光沿着时间轴上溯,就会发现一个问题。当在内存中分配空间,创建对象的时候,毫无疑问地,必须要知道申请多大的空间。显然,这不会是一个定值,因为对于不同的对象,需要不同的空间,一个整数对象和一个字符串对象所需的空间肯定不同。那么,对象所需的内存空间的大小的信息到底在哪里呢?在对象头中显然没有这样的信息。

实际上,内存空间大小这样的对象的元信息是与对象所属类型密切相关的,因此它一定会出现在与对象所对应的类型对象中。现在我们可以来详细考察一下类型对象_typeobject

[object.h]

typedef struct _typeobject {

    PyObject_VAR_HEAD

    char *tp_name; /* For printing, in format "<module>.<name>" */

    int tp_basicsize, tp_itemsize; /* For allocation */

 

    /* Methods to implement standard operations */

    destructor tp_dealloc;

printfunc tp_print;

……

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;

    ternaryfunc tp_call;

    ……

} PyTypeObject;

 

 

 

_typeobject的定义中包含了许多的信息,主要可以分为四类:

1.类型名,tp_name,主要是Python内部以及调试的时候使用;

2.创建该类型对象是分配内存空间的大小的信息,即tp_basicsizetp_itemsize

3.与该类型对象相关联的操作信息,比如hashfunctp_hash就指明对于该类型的对象,如何生成其hash值。在Object.h中可以看到,hashfunc实际上是一个函数指针:typedef long (*hashfunc)(PyObject *); _typeobject中,包含了大量的函数指针,这些函数指针将用来指定某个类型的操作信息。这些操作主要分为标准操作(dealloc, print, compare),标准操作族(numbers, sequences, mappings),以及其他操作(hash, buffer, call…)。

4.我们在下边将要描述的类型的类型信息。

有趣的是我们在_typeobject的头部发现了PyObject_VAR_HEAD,这意味着类型实际上也是一个对象。我们知道在Python中,每一个对象都是对应一种类型的,那么一个有趣的问题就出现了,类型对象的类型是什么呢?这个问题听上去很绕口,实际上确非常重要,对于其他的对象,可以通过与其关联的类型对象确定其类型,那么通过什么来确定一个对象是类型对象呢?答案就是PyType_Type

[typeobject.c]

PyTypeObject PyType_Type = {

    PyObject_HEAD_INIT(&PyType_Type)

    0,                  /* ob_size */

    "type",                 /* tp_name */

    sizeof(PyHeapTypeObject),       /* tp_basicsize */

    sizeof(PyMemberDef),            /* tp_itemsize */

    ……

    PyObject_GC_Del,                /* tp_free */

    (inquiry)type_is_gc,            /* tp_is_gc */

};

 

 

 

前面提到,在Python中,每一个对象它的开始部分都是一样的。每一个对象都将自己的引用计数,类型信息保存在开始的部分中。为了方便对这部分内存的初始化,Python中提供了几个有用的宏:

[object.h]

#ifdef Py_TRACE_REFS

#define _PyObject_EXTRA_INIT 0, 0,

#else

#define _PyObject_EXTRA_INIT

#endif

 

#define PyObject_HEAD_INIT(type)    /

    _PyObject_EXTRA_INIT        /

    1, type,

 

 

 

再回顾一下PyObjectPyVarObject的定义,初始化的动作就一目了然了。实际上,这些宏在类型对象的初始化中被大量地使用着。

如果以一个整数对象为例,可以更清晰地看到一半的类型对象和这个特立独行的PyType_Type对象之间的关系:

[intobject.c]

PyTypeObject PyInt_Type = {

    PyObject_HEAD_INIT(&PyType_Type)

    0,

    "int",

    sizeof(PyIntObject),

    ……

};

现在我们可以放飞想象,看到一个整数对象在运行时的抽象的表示了,下图中的箭头表示ob_type

 

 

3.      对象间的继承和多态

通过PyObject和类型对象,Python利用C语言完成了C++所提供的继承和多态的特性。前面提到,在Python中所有的内建对象(PyIntObject等)和内部使用对象(PyCodeObject等)的最开始的内存区域都拥有一个PyObject。实际上,这一点可以视为PyIntObjectPyCodeObject等对象都是从PyObject继承而来。

Python创建一个对象,比如PyIntObject对象时,会分配内存,进行初始化。然后这个对象会由一个PyObject*变量来维护,而不是通过一个PyIntObject*指针来维护。其它对象也与此类似,所以在Python内部各个函数之间传递的都是一种范型指针PyObject*。这个指针所指的对象究竟是什么类型的,不知道,只能从指针所指对象的ob_type域判断,而正是通过这个域,Python实现了多态机制。

考虑下面的代码:

void Print(PyObject* object)

{

    object->ob_type->tp_print(object);

}

如果传给Print的指针实际是一个PyIntObject*,那么就会调用PyIntObject对象对应的类型对象中定义的输出操作,如果传给Print的指针实际是一个PyStringObject*,那么就会调用PyStringObject对象对应的类型对象中定义的输出操作。可以看到,这里同一个函数在不同情况下表现出了不同的行为,这正是多态的核心所在。

object.c中,Python实现了一些对于类型对象中的各种操作的简单包装,从而为Python运行时提供了一个统一的多态接口层:

[object.c]

long PyObject_Hash(PyObject *v)

{

    PyTypeObject *tp = v->ob_type;

    if (tp->tp_hash != NULL)

        return (*tp->tp_hash)(v);

    ……

}

4.      引用计数

CC++中,程序员被赋予了极大的自由,可以任意地申请内存。但是权利的另一面则对应着责任,程序员必须自己负责将申请的内存释放,并释放无效指针。可以说,这一点正是万恶之源,大量的内存泄露和悬空指针的bug由此而生,如黄河泛滥一发不可收拾 :)

现代的开发语言中一般都选择由语言本身负责内存的管理和维护,即采用了垃圾收集机制,比如JavaC#。垃圾收集机制使开发人员从维护内存分配和清理的繁重工作中解放出来,但同时也剥夺了程序员与内存亲密接触的机会,并付出了一定的运行时效率作为代价。现在看来,随着垃圾收集机制的完善,对时间要求不是非常高的程序完全可以通过使用垃圾收集机制的语言来完成,这部分程序占了这个星球上大多数的程序。这样做的好处是提高了开发效率,并降低了bug发生的机率。Python同样也内建了垃圾收集机制,代替程序员进行繁重的内存管理工作,而引用计数正式Python垃圾收集机制的一部分。

Python通过对一个对象的引用计数的管理来维护对象在内存中的生存。我们知道在Python中每一个东西都是一个对象,都有一个ob_refcnt变量,正是这个变量维护着该对象的引用计数,从而也最终决定着该对象的生生灭灭。

Python中,主要是通过Py_INCREF(op)Py_DECREF(op)两个宏来增加和减少一个对象的引用计数。当一个对象的引用计数减少到0之后,Py_DECREF将调用该对象的析构函数(deallocator function)来释放该对象所占有的内存和系统资源。注意这里的析构函数借用了C++的词汇,实际上这个析构动作是通过在对象对应的类型对象中定义的一个函数指针来刻画的,还记得吗?就是那个tp_dealloc

如果熟悉设计模式中Observer模式,可以看到,这里隐隐约约透着Observer模式的影子。在ob_refcnt减为0之后,将触发对象销毁的事件;从Python的对象体系来看,各个对象又提供了不同的事件处理函数,而事件的注册动作正是在各个对象对应的类型对象中静态完成的。

对于这两个宏的参数op来说,不允许op是一个指向空对象的指针(NIL),如果op是一个NIL,那么必须使用Py_XINCREF/Py_XDECREF这一对宏。

PyObject中我们看到ob_refcnt是一个32位的整形变量,这实际是一个Python所做的假设,即对一个对象的引用不会超过一个整形变量的最大值。一般情况下,如果不是恶意代码,这个假设显然是不会被突破的。

需要注意的是,在Python的各种对象中,类型对象是超越引用计数规则的。类型对象“跳出三界外,不再五行中”,永远不会被析构。每一个对象中指向类型对象的指针不被视为对类型对象的引用。

在每一个对象创建的时候,Python提供了一个_Py_NewReference(op)宏来将对象的引用计数初始化为1

Python的源代码中可以看到,在不同的编译选项下(Py_REF_DEBUG, Py_TRACE_REFS),引用计数的宏还要做许多额外的工作。下面展示的代码是Python在最终发行时这些宏所对应的实际的代码:

[object.h]

/* Without Py_TRACE_REFS, there's little enough to do that we expand code

 * inline.

 */

#define _Py_NewReference(op) ((op)->ob_refcnt = 1)

 

 

 

#define _Py_Dealloc(op) ((*(op)->ob_type->tp_dealloc)((PyObject *)(op)))

 

 

 

#define Py_INCREF(op) ((op)->ob_refcnt++)

 

 

 

#define Py_DECREF(op)                   /

    if (--(op)->ob_refcnt != 0)         /

        ;            /

    else                        /

        _Py_Dealloc((PyObject *)(op))

 

 

 

/* Macros to use in case the object pointer may be NULL: */

#define Py_XINCREF(op) if ((op) == NULL) ; else Py_INCREF(op)

#define Py_XDECREF(op) if ((op) == NULL) ; else Py_DECREF(op)

 

 

 

在一个对象的引用计数减为0时,与该对象对应的析构函数就会被调用,但是要特别注意的是,调用析构函数并不意味着最终一定会调用free释放内存空间,如果真是这样的话,那频繁地申请、释放内存空间会使Python的执行效率大打折扣(更何况Python已经多年背负了人们对其执行效率的指责:)。一般来说,Python中大量采用了内存对象池的技术,使用这种技术避免频繁地申请和释放内存空间。因此在析构时,通常都是将对象占用的空间归还到内存池中。这一点在接下来对Python内建对象的实现中可以看得一清二楚。

5.      Python对象的分类

我们将Python的对象从概念上大致分为四类,需要指出的是,这种分类并不一定完全正确,不过是提供一种看待Python中对象的视角而已:

l        Math :数值对象

l        Container :容纳其他对象的集合对象

l        Composition :表示程序结构的对象

l        Internal Python解释器在运行时内部使用的对象

2列出了我们的对象分类体系,并给出了每一个类别中的一些实例:

6.      通向Python之路

Python源码的剖析将分为四部分。

1.静态对象剖析:首先我们会分析静态的对象,Math对象和Container对象,深刻理解这些对象对我们理解Python解释器的运行会有很大的帮助,同时,对我们编写Python代码也将大有裨益,在编写Python代码时,你会清晰地意识到系统内部这些对象将如何运作,变化。当然,我们并不会分析所有的Python对象,而是选取使用最频繁的四种对象:PyIntObject, PyStringObject, PyListObject, PyDictObject进行剖析。

2.运行时剖析:在分析完静态的对象之后,我们将进入Python解释器,在这里我们会详细地考察Python的字节码(byte code)以及解释器对字节码的解释和执行过程。这部分将完整地展现Python中所有的语法结构,如一般表达式,控制流,异常流,函数,类等等的字节码层面的实现细节。同时,在这部分,我们会考察大部分的Python内部对象。

3.编译期剖析:这部分没什么好打广告的了,目标明确,对象清晰,但是难度呢,绝不简单 :)

4.运行环境剖析:这部分将考察从激活PythonPython准备就绪,可以接受用户输入或者执行脚本文件,这段时间内,Python如何建立自己的运行环境,并建立了怎样的运行环境,呵呵透露一下,想想Python那个庞大的builtin函数集合,这些就是这部分考察的重点。

阅读完这些内容之后,对于Python,你应该是了如指掌了,在以后编写Python代码时,你的脑子里甚至可以出现Python解释器将如何一步步解释你的代码的情形。当然,这只是我写作本书的副产品。这本书诞生的真正原因只有一个,兴趣,我对Python的实现有浓厚的兴趣。这本书也只是第一步,希望以后还能继续对Python系列,如IronPythonJythonPyPy的探索,当然,对于其他动态语言,比如Ruby的探索,我希望也会有时间去做。如果你对动态语言的实现有兴趣,你一定会喜欢本书;如果你还没有兴趣,希望它能唤起你的兴趣 :)

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页