了解了Python类对象和实例对象的在C中的结构体之后,继续探究一下Python是如何利用这些结构体进行对象的创建和销毁的。

C API

首先介绍一下Python提供的C API。

Python是由C语言编写的,对外提供了C API,让用户可以从C环境中与其交互。Python内部也使用了大量的这种API。

C API分为两个类型:

泛型API:

泛型API与类型无关,属于抽象对象层(Abstract Object Layer),简称AOL。这类API参数是PyObject*,可处理任意类型的对象,API内部根据对象类型区别处理。

比如Python里面常用的print打印函数:

int PyObject_Print(PyObject *o, FILE *fp, int flags);

其第一个参数类型是PyObject*,可以是任意类型的对象。传入不同的对象,而后根据不同对象的对象类型,再决定如何输出对象。

特型API:

特型API与类型相关,属于具体对象层(Concrete Object Layer),简称COL。这类API只能作用于某种类型的对象,Python内部为每一种内置对象提供了这样一组API。

比如创建一个浮点对象并进行初始化:

PyObject * PyFloat_FromDouble(double fval)

 

对象的创建

我们已经知道,对象的元数据是保存在对用的类型对象中的,而且元数据也包括对象如何创建的信息。那么可以推断,实例对象其实是由类型对象创建的。

其实,不管创建对象的流程如何,最终的关键步骤都是分配内存。如果是内建对象,Python提供了C API,可以为实例对象直接分配了内存并执行了初始化的操作。

但对于用户自定义的类型,比如class Dog()而言,Python肯定无法事先提供PyDog_New这样的C API了。如此,就只能通过Dog所对应的类型对象创建实例对象了。至于具体的数据,比如分配多少内存,如何进行初始化,答案就需要在类型对象中寻找了。

总结一下,Python内部一般通过两个方法创建对象:

  • 通过C API,例如PyFloat_FromDouble,多用于内建类型;
  • 通过类型对象,多用于自定义的类。

用代码表示这两种方法,如C API的调用:

>>> pi = 3.14
>>> pi
3.14
>>> type(pi)
<class 'float'>

如上,创建变量pi并进行赋值,pi直接变成了浮点类型的实例对象,这种创建方式,便是直接调用C API,利用Python对内建对象的无所不知,直接分配了内存并进行初始化。

当然,我们还可以通过第二种方法,利用类型对象创建实例对象。其实这是一种更加通用的流程,同时支持内置类型和自定义类型。依旧以浮点对象为例,可以通过浮点类型PyFloat_Type来创建:

>>> pi = float('3.14')
>>> pi
3.14
>>> type(pi)
<class 'float'>

 如上,便是调用类型对象float,实例化一个浮点实例pi。在Python中,此类可以被调用的对象就是 可调用对象。

那么既然对象可以被调用,那么当对象被调用时,执行的是什么函数呢?既然实例对象的元信息保存在类型对象中,而float类型对象的类型是type,那么此种调用执行的方法应该就在type的代码中,查看PyType_Type,在它的结构体中,存在一个tp_call字段,这是一个指针函数。

PyTypeObject PyType_Type = {

    // ...
    (ternaryfunc)type_call,                     /* tp_call */

    // ...
};

 当实例对象被调用时,便执行tp_call字段保存的处理函数。

如果利用float('3.14')进行浮点数创建,那么在C层面等价于:

PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)

即:

PyType_Type.tp_call(&PyFloat_Type, args, kwargs)

 最终执行type_call函数:

type_call(&PyFloat_Type, args, kwargs)

 查找type_call的代码,在文件Objects/typeobject.c中,关键代码如下:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;
  
    // ......

    if (type->tp_new == NULL) {
        _PyErr_Format(tstate, PyExc_TypeError,
                      "cannot create '%s' instances", type->tp_name);
        return NULL;
    }

    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    // ........  
  
    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(_PyErr_Occurred(tstate));
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    return obj;
}

 可以大致阅读一下代码,跟踪传入的参数,将会发现其有两个核心的方法调用这些参数,分别是tp_new和tp_init:

  • 调用类型对象tp_new函数指针,为对象申请内存;
  • 一定条件下(通常都是满足),调用类型对象tp_init函数指针对对象进行初始化。

其实,这两个函数指针,对应Python的类方法为__new__()以及__init__(),分别负责类的创建和数据初始化。

至此,对象的创建流程就很清晰了:

class生命周期 python python对象生命周期_Python

总结一下,调用可调用对象float类型对象,即调用float可以创建实例对象:

  1. 调用float,Python最终执行的是类型对象type的tp_call函数;
  2. tp_call函数一定会调用float的tp_new函数为实例对象分配内存空间;
  3. tp_call函数在必要时将继续调用float的tp_init函数对实例对象进行 初始化。

 

对象的多态性

 根据上面tp_call的代码,可以看出,tp_new返回的是一个PyObject*变量,而非PyFloatObject*变量。其实在Python内创建一个对象,比如PyFloatObject,会分配内存并进行初始化,此后,Python内部统一通过一个PyObject*变量来保存和维护这个对象。

通过PyObject*变量保存和维护对象,可以实现更抽象的上层逻辑,而不用关心对象的实际类型和实现细节。

以对象哈希值为例,其函数接口为(这种便是泛型C API):

Py_hash_t PyObject_Hash(PyObject *v);

该函数可以计算任意对象的哈希值,不管对象的类型是什么,比如计算浮点型:

PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Hash(fo);
>>> hash(3.14)
322818021289917443

亦或者是整数对象:

PyObject *lo = PyLongObject_FromLong(100);
PyObject_Hash(lo);
>>> hash(100)
100

然而,对象类型不同,其行为也千差万别,哈希值计算方法也是如此,根据上面的运行结果,可见浮点和整数的hash计算方式是完全不同的。

查看PyObject_Hash函数,了解是如何做到的:

Py_hash_t
PyObject_Hash(PyObject *v)
{
    PyTypeObject *tp = Py_TYPE(v);
    if (tp->tp_hash != NULL)
        return (*tp->tp_hash)(v);
    /* To keep to the general practice that inheriting
    * solely from object in C code should work without
    * an explicit call to PyType_Ready, we implicitly call
    * PyType_Ready here and then check the tp_hash slot again
    */
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            return -1;
        if (tp->tp_hash != NULL)
            return (*tp->tp_hash)(v);
    }
    /* Otherwise, the object can't be hashed */
    return PyObject_HashNotImplemented(v);
}

该函数通过Py_TYPE方法找到对象的类型,而后通过类型对象的tp_hash函数指针,调用对应的哈希值计算函数,也就是说,函数根据对象的类型,调用不同版本的tp_hash函数。其实这就是多态的理念,利用ob_type字段,实现了C层面上的对象的多态的特型。

 

对象的行为

不同对象的行为可能不同,比如不同对象的哈希值算法就不一样,这个由类型对象中tp_hash字段对应的函数指针所决定的。除了tp_hash,我们看到PyTypeObject结构体其实还定义了很多函数指针,这些指针都会指向某个函数或者为空。这些函数指针可以看做是类型对象中定义的操作,这些操作决定了实例对象在运行时的行为。

 

引用计数

在C/C++语言中,程序员可以任意申请内存,并自由管理,但这容易引起内存泄露、野指针、越界访问等问题。针对这种问题,许多语言都采用语言本身负责内存的管理的办法(垃圾回收机制),比如Java、Golang等。虽然程序本身管理内存会在一定程度上牺牲部分执行效率和自由度,但也可以使得开发者更加关注业务的本身。

Python垃圾回收机制的关键是对象的引用计数,它决定了一个对象的生死。通过PyObject这个公共头结构体,我们知道每个Python对象都有一个ob_refcnt字段,记录着对象当前的引用计数。当对象被其他地方引用时,ob_refcnt加一;当引用解除时,ob_refcnt减一。当ob_refcnt为零,说明对象已经没有被引用,这时便可将其回收。

>>> import sys
>>> a = 3.14
>>> sys.getrefcount(a)
2

如上:创建变量a,此时的引用计数为1,利用sys.getrefcount方法查看对象的引用计数,获得结果为2,这里多了1,是因为对象被方法调用时,也属于被引用,所以计数加一。之所以函数调用会加一,是因为函数执行过程中,会先新建栈帧,栈帧用于记录函数执行上下文,局部变量就保存在栈帧中,当getrefcount调用变量a,那么变量a就会作为函数内的局部变量保存在栈帧中,所以计数会加一为2,当结果输出,调用返回,虚拟机开始销毁函数执行环境,也就是栈帧对象,这样对象的引用计数就会减一。

>>> b = a
>>> sys.getrefcount(a)
3

思考一下,如果查询变量b的引用计数,那么结果会是多少?结果是3。因为赋值操作实际就是具有与对方相同的指针地址,也就是说a和b具有了相同的结构体,那么结构体内的计数字段自然也就相同。

>>> l = [a]
>>> l
[3.14]
>>> sys.getrefcount(a)             // 被列表引用,计数加一
4
>>> del b
>>> sys.getrefcount(a)             // 删除变量b对结构体的指向,计数减一
3
>>> l.clear()
>>> sys.getrefcount(a)             // 删除列表对结构体的引用,计数减一
2
>>> del a                          // 删除变量a,计数归0

在Python中,有许多场景涉及到计数的调整:

  • 容器操作;
  • 变量赋值;
  • 函数操作传递;
  • 属性操作;

为此,Python定义了两个相关的重要宏,用户维护对象的计数。

其一是Py_INCREF,将对象引用计数加一:

#define Py_INCREF(op) (                         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
    ((PyObject *)(op))->ob_refcnt++)

另一个是Py_DECREF宏,将对象引用计数减一,并在计数为0时回收对象:

#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);                \
    } while (0)

当一个对象的引用计数为0时,Python便会调用对象对应的析构函数销毁对象,但这并不意味着对象内存一定会回收。为了提高内存分配效率,Python为一些常用对象维护了内存池,对象回收后内存进入内存池中,以供下次使用,这样可以避免频繁申请/释放内存。