# 对象模型概述

# 一切皆对象

  • Python中一切皆对象,因此变量实际只是一个指向对象的指针-CPython

    • 对不可变对象进行修改操作时实际是先解绑原先的对象然后创建一个新的对象再绑定
  • 不区分基本类型和对象,所有基本类型均由对象实现

    • 类型也由对象实现-Type类
    • 可以简单分为类型对象和实例对象两个体系
  • 对象分为可变对象和不可变对象

# 类型对象、实例对象体系

  • 类型对象

    • 类型对象也是对象,类型是Type,所有类型如Int、Float、自己用class关键字定义的类等都是Type类的实例,这些实例在程序中作为全局变量存在
    • Type类型的类型是自己-Type是所有类型的类型
    • 类型对象中保存着对象的元数据,描述对象的类型、内存信息、支持的操作等 PyTypeObject
  • 实例对象

    • 实例的类型就是类型对象

    • 实例对象由类型对象进行创建

      • 对于内建类型一般使用内置的CAPI直接创建,而不是走常见的new、init流程
      • 对于自定义类型则一般通过类型对象进行创建
      • 但实际上两者执行的逻辑是一致的
  • obejct对象

    • 所有对象的基类都是object-__base__​属性

      • type的基类也是object
      • object在初始化时不会设置__base__​属性-防止循环继承链
    • object的类型是type,object没有基类-防止继承链死循环

print(type(int))            # <class 'type'>
print(type(type(int)))      # <class 'type'>
print(type(object))         # <class 'type'>
print(int.__bases__)        # (<class 'object'>,)
print(type.__bases__)       # (<class 'object'>,)
print(object.__bases__)     # ()

​​image​​

image

# 对象生命周期

# 对象的创建

  • 首先明确以下两点

    • 类型对象存储着类型的元信息
    • 自定义的实例对象通过类型对象进行创建
  • 对象的创建

    • 首先执行type​类型对象中的tp_call​函数
    • tp_call​函数调用_type​的type_new​函数为实例对象分配内存空间
    • 必要时tp_call​函数调用_type​的type_init​函数对实例对象进行初始化
// 例如float("3.14")
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)
// 等价于
PyType_Type.tp_call(&PyFloat_Type, args, kwargs)
// 等价于-实际调用的是
type_call(&PyFloat_Type, args, kwargs)

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

    // ...
    obj = type->tp_new(type, args, kwds); // 申请内存
    obj = _Py_CheckFunctionResult((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());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    return obj;
}

# 对象的多态

  • 当对象完成创建后,Python内部统一通过PyObject*​类型来保存和维护这个对象

  • 以此实现更抽象的上层逻辑而不关心对象的实际类型和实现细节-详见CAPI

    例如假设有一个计算对象哈希值的函数接口:

    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);
    }
    

# 对象的行为

# 标准操作集

对象的每个行为最终会落实到类型对象的一个函数指针之上。

可以以对象行为为依据,对对象进行分类:

  1. 数值型对象:int、float
  2. 序列型对象:str、bytes、tuple、list
  3. 关联性对象:dict

Python以此为依据,为每个类别定义了一个标准操作集:

  • PyNumberMethods 结构体定义了 数值型 操作;
  • PySequenceMethods 结构体定义了 序列型 操作;
  • PyMappingMethods 结构体定义了 关联型 操作;

只要类型对象提供了相关操作集,实例对象便具备相应的行为:

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 */

    // ...
    /* 标准操作集:Method suites for standard classes */

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

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

    // ...
} PyTypeObject;

// 例如float对象
static PyNumberMethods float_as_number = {
    float_add,          /* nb_add */
    float_sub,          /* nb_subtract */
    float_mul,          /* nb_multiply */
    float_rem,          /* nb_remainder */
    float_divmod,       /* nb_divmod */
    float_pow,          /* nb_power */
    // ...
};

PyTypeObject PyFloat_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "float",
    sizeof(PyFloatObject),

    // ...
    &float_as_number,                           /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */

    // ...
};

# 引用计数

  • Python定义了宏Py_INCREF(op)​和Py_DECREF(op)​来维护对象引用计数-Py_INCREF&&Py_DECREF
  • 同时Python 为一些常用对象维护了内存池, 对象回收后内存进入内存池中,以便下次使用,由此 避免频繁申请、释放内存

# 垃圾回收机制

  • python中一切皆对象,所有的变量本质上都是对象的一个指针。

  • Python中的自动化内存管理以引用计数为基础,同时以标记-清除和分代收集为辅。

  • 我们通过为对象设置引用计数(指针数)判断对象被利用(计数为0则需要回收)。

    • 对象被创建、引用、作为参数传出、存入容器时引用计数+1

    • 对象别名被显式销毁、别名被赋予新的对象,对象离开作用域、对象所在容器被销毁或对象被从容器中删除时引用计数-1

    • 可以通过sys​模块的getrefcount​函数来获得对象的引用计数

      • 对象作为函数参数传递时会将引用计数+1避免对象被提前销毁,在返回后再将引用技术-1。因此对象刚创建后getrefcount​得到的结果也是2。
    • 循环引用或者引用环的情况下,虽然引用计数不为0,但是仍然应该被回收,python垃圾会使机制能处理这一情况。可以显式调用 gc.collect() ,来启动垃圾回收。

  • 引用计数是其中最简单的实现,不过这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收,因为循环引用可能导致内存泄漏问题。

  • Python 使用标记清除(mark-sweep) 算法和分代收集(generational),来启用针对循环引用的自动垃圾回收和提高垃圾回收的效率

    • 标记清除算法。对于一个有向图,如果从一个节点出发进行遍历,并标记其经过的所有节点;那么,在遍历结束后,所有没有被标记的节点,我们就称之为不可达节点。显而易见,不可达节点的存在是没有任何意义的,因此我们需要对它们进行垃圾回收。

      为什么标记清除算法能解决循环引用问题:经过一次次垃圾回收,循环引用的环一定是不可达的。

      learn.lianglianglee.com/... (opens new window)

    • 分代收集算法。

      1. 在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。
      2. 分代收集基于的思想是,新生的对象更有可能被垃圾回收,而存活更久的对象也有更高的概率继续存活。
      3. 分代回收的基本思想是Python 将所有对象分为三代,从第0代到第2代扫描频率依次降低。刚刚创立的对象是第 0 代;经过一次垃圾回收后,依然存在的对象,便会依次从上一代挪到下一代。而每一代启动自动垃圾回收的阈值,则是可以单独指定的。当垃圾回收器中新增对象减去删除对象达到相应的阈值(一定数量)时,即新增对象的数量达到了一定值,就会对这一代对象启动垃圾回收。对象会在年龄达到一定程度后,被放到年龄较大的队列中,并且随着年龄的增大,它们被扫描的频率也会降低。这样,在大多数情况下,垃圾回收器只需要扫描年龄较小的对象,可以大大提高回收的效率。