Python对象初探(《Python源码剖析》笔记一)

阅读 178
收藏 13
2017-06-29
原文链接:zhuanlan.zhihu.com
这是我的关于《Python源码剖析》一书的笔记的第一篇。Learn Python by Analyzing Python Source Code · GitBook

Python对象初探

在Python中,一切皆对象。这一章中我们将会了解对象在C的层面究竟是如何实现的,同时还将了解到类型对象在C的层面是如何实现的。这样,我们将对Python对象体系有一个大概的了解,从而进入具体的讨论。


Python内的对象


在Python中,对象就是为C中的结构体在堆上申请的一块内存。一般来说,对象是不能被静态初始化的,并且也不能在栈空间上生存。唯一的例外是类型对象,Python中所有的内建类型对象都是被静态初始化的。

在Python中,一个对象一旦被创建,它在内存中的大小就是不变的了。这就意味着那些需要容纳可变长度数据的对象只能在对象内维护一个指向一块可变大小的内存区域的指针。

一个对象维护着一个“引用计数”,其在一个指向这个对象的指针复制或删除时增加或减少。当这个引用计数变为零时,也就是说已经没有任何指向这个对象的引用,这个对象就可以从堆上被移除。

一个对象有着一个类型(type),来确定它代表和包含什么类型的数据。一个对象的类型在它被创建时是固定的。类型本身也是对象。一个对象包含一个指向与之相配的类型的指针。类型自己也有一个类型指针指向着一个表示类型对象的类型的对象“type”,这个type对象也包括一个类型指针,不过是指向它自己的。

基本上Python对象的特性就是这些,那么,在C的层面上,一个Python对象的这些特性是如何实现的呢?


对象机制的基石——PyObject


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


[object.h]
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;
#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

从上面的代码中,我们可以看到前面我们提到的对象的特性的实现:

变量ob_refcnt与Python的内存管理机制有关,它实现了基于引用计数的垃圾收集机制。对于某一个对象A,当有一个新的PyObject *引用该对象时,A的引用计数应该增加;而当这个PyObject *被删除时,A的引用计数应该减少。当A的引用计数减少到0时,A就可以从堆上被删除,以释放出内存供别的对象使用。

在ob_refcnt之外,我们注意到ob_type是一个指向_typeobject结构体的指针。这个结构体是用来指定一个对象类型的类型对象。 所以,我们可以看到,在Python中,对象机制的核心十分简单——引用计数和类型信息。 PyObject中定义了每个Python对象中都会有的东西,它们将出现在每个对象所占有的内存的最开始的字节中。不过,这可不代表每个Python对象就有这么点东西,事实上,除此之外,每个对象还会根据自己本身类型的不同而包括着不同的内容。


定长对象和变长对象


在Python2中,一个int类型的对象的值在C中的类型是不变的(int),所以它在内存中占据的大小也是不变的。但是一个字符串对象事先我们根本不可能知道它所维护的值的大小。在C中,根本就没有“一个字符串”的概念,字符串对象应该维护“n个char型变量”。不只是字符串,list等对象也应该维护“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;

我们把类似Python2中的int对象这样不包含可变长度的对象称为“定长对象”,而字符串对象这样包含可变长度数据的对象称为“变长对象”。

为什么要强调Python2中的int对象呢?因为在Python3中,int类型的底层实现直接使用了Python2中的long类型的底层实现,也就是说,现在的int是以前的long类型,而以前的int类型已经不复存在。而long类型实际是一个变长对象。

变长对象通常都是容器,ob_size这个成员实际上就是指明了变长对象中一共容纳了多少个元素。

从PyVarObject的定义可以看出,变长对象实际就是在PyObject对象后面加了个ob_size,因此,对于任意一个PyVarObject,其所占用的内存开始部分的字节就是一个PyObject。在Python内部,每一个对象都拥有着相同的对象头部。这就使得在Python中,对对象的引用变得非常的统一,我们只需要用一个PyObject *指针就可以引用任意的一个对象。


类型对象


我们前面提到,一个对象就是在内存中维护的一块内存。显然,对于不同的对象,它在内存中占用的大小是不同的,但在PyObject的定义中我们却未见到关于这方面的信息。实际上,占用内存空间的大小是对象的一种元信息,这样的元信息是与对象所属类型密切相关的,因此它一定会出现在与对象所对应的类型对象中。这个类型对象就是_typeobject。


[object.h]
typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    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;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

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

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

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

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    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;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;

#ifdef COUNT_ALLOCS
    /* these must be last and never explicitly initialized */
    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;

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

  • 类型名,tp_name,主要是Python内部和调试的时候使用。


  • 创建该类型对象时分配内存空间大小的信息,即tp_basicsize和tp_itemsize。


  • 与该类型对象相关联的操作信息。


  • 类型的类型信息。


对象的创建


假如我们命令Python创建一个整数对象,Python内部究竟如何创建呢?

一般来说,Python会有两种方法。第一种是通过Python C API来创建,第二种是通过类型对象PyLong_Type。(在Python3中,已经没有了long类型,int类型的底层实现都是通过以前的long类型来实现的)

Python的C API分成两类:

一类称为范式的API,或者称为AOL(Abstract Object Layer)。这类API都具有诸如PyObject__**的形式,可以应用在任何Python对象身上。对于创建一个整数对象,我们可以采用如下的表达式:PyObject_longObj = PyObject_New(PyObject,&PyLong_Type)。

另一类是与类型相关的API,或者称为COL(Concrete Object Layer)。这类API通常只能作用在某一种类型的对象上,对于每一种内建对象,Python都提供了这样的一组API。比如整数对象,我们可以使用下列的API来创建,PyObject* longObj = PyLong_FromLong(10)。

不论采用那种C API,Python内部最终都是直接分配内存。但是对于用户自定义的类型,如果要创建它的实例对象,Python就不可能事先提供类似Py_New这样的API。对于这种情况,Python会通过它所对应的类型对象来创建实例对象。所有的类都是以object为基类的。


对象的行为


在PyTypeObject中定义了大量的函数指针,这些函数指针最终都会指向某个函数,或者指向NULL。这些函数指针可以视为类型对象中所定义的操作,而这些操作直接决定着一个对象在运行时所表现出的行为。

在这些操作信息中,有三组非常重要的操作族,在PyTypeObject中,它们是tp_as_number、tp_as_sequence、tp_as_mapping。它们分别指向PyNumberMethods、PySequenceMethods和PyMappingMethods函数族。

我们可以看看PyNumricMethods函数族:


typedef PyObject * (*binaryfunc)(PyObject *, PyObject *);

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;
    ……
} PyNumberMethods;

可以看出,一个函数族中包括着一族函数指针,它们指向的函数定义了作为一个数值对象所应该支持的操作。

对于一种类型来说,它完全可以同时定义三个函数族中的所有操作。


类型的类型


类型对象的类型是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 */
};

所有用户自定义class所对应的PyTypeObject对象都是通过这个对象创建的。

在Python层面,这个神奇的对象PyType_Type被称为metaclass。


Python对象的多态性


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

这样我们就能够理解,一个Python对象在程序运行时可以动态改变它的类型。也就是说,一个实例对象x的代表着它的类型的魔法属性__class__并不是只读的,而是可以动态改变的。


引用计数


Python通过对一个对象的引用计数的管理来维护对象在内存中的存在与否。在Python中,主要是通过Py_INCREF(op)和Py_DECREF(op)两个宏来增加和减少一个对象的引用计数。当一个对象的引用计数减少到0之后,Py_DECREF将调用该对象的析构函数(类型对象中定义的一个函数指针——tp_dealloc)来释放该对象的内存和系统资源。

在Python的各种对象中,类型对象是超越引用计数规则的。类型对象永远不会被析构。每一个对象中指向类型对象的指针不被视为对类型对象的引用。

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

在一个对象的引用计数减为0时,与该对象对应的析构函数就会被调用,但要注意的是,调用析构函数并不意味着最终一定会调用free释放内存空间。一般来说,Python中大量采用了内存对象池的技术,使用这种技术可以避免频繁地申请和释放内存空间。因此在析构时,通常都是将对象占用的空间归还到内存池中。


Python对象的分类



  • Fundamental对象:类型对象


  • Numeric对象:数值对象


  • Sequence对象:容纳其他对象的序列集合对象


  • Mapping对象:类似C++中map的关联对象


  • Internal对象:Python虚拟机在运行时内部使用的对象。

评论