1. 对象的创建

首先我们需要观察思考:
  • Python 内部是如何从无到有创建一个浮点对象的
  • Python 又是怎么知道该如何将它打印到屏幕上的呢?
>>> pi = 3.14

>>> print(pi)
3.14

下面以 floatl类型为例子,对应C实体是 PyFloat_Type
  • 首先来介绍C API

  • Python 是用 C 写成的,对外提供了 C API ,让用户可以从 C 环境中与其交互。 Python 内部也大量使用这些 API ,为了更好研读源码,先系统了解 API 组成结构很有必要。 C API 分为两类: 泛型 API 以及 特型 API

    • (1)泛型 API
      • 泛型 API 与类型无关,属于 抽象对象层 ( Abstract Object Layer ),简称 AOL 。 这类 API 参数是 PyObject* ,可处理任意类型的对象, API 内部根据对象类型区别处理
      • 我的理解就是通用的接口, 就像人一样,每个人都需要 吃饭,睡觉。 吃饭,睡觉,就是泛型API,处理任意类型的对象(人),所谓抽象,就是不是具体指某个实际对象,而是抽提出来的公共部分,所称为 抽象。(看过java基础,对抽象类概念的理解)
      • 以对象打印函数为例子:
int
PyObject_Print(PyObject *op, FILE *fp, int flags)

接口第一个参数为待打印对象,可以是任意类型的对象,因此参数类型是 PyObject* 。 Python 内部一般都是通过 PyObject* 引用对象,以达到泛型化的目的
* 对于任意类型的对象,均可调用 PyObject_Print 将其打印出来(抽象)

// 打印浮点对象
PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Print(fo, stdout, 0);

// 打印整数对象
PyObject *lo = PyFloatObject_FromLong(100);
PyObject_Print(lo, stdout, 0);

PyObject_Print 接口内部根据对象类型,决定如何输出对象。

  • (2)特型 API
    • 特型 API 与类型相关,属于 具体对象层 ( Concrete Object Layer ),简称 COL 。 这类 API 只能作用于某种类型的对象,例如浮点对象 PyFloatObject 。 Python 内部为每一种内置对象提供了这样一组 API
    • 很容易理解,就是针对 某个类型,提供特定的方法接口。
      例如:
PyObject *
PyFloat_FromDouble(double fval)

PyFloat_FromDouble 创建一个浮点对象,并将它初始化为给定值 fval 。


对象的创建
到这里我们终于要讲对象的创建了! 当然前面的介绍是少不了的
  • 经过上面一节的学习,我们知道了 元数据的概念,它保存在类型对象中
    • 1.元数据 保存着创建对象的 内存等在这里插入代码片信息
    • 2.支持的操作
    • 3 也包括对象如何创建的信息。
  • 实际上,不管创建爱你对象的流程如何,最终的关键步骤都是 分配内存
总结起来python内部通过两种方式创建对象
  • (1)通过 C API ,例如 PyFloat_FromDouble ,多用于内建类型;比如 上面提到的 float
  • (2) 通过类型对象,例如 Dog ,多用于自定义类型
  • 通过类型对象创建实例对象,是一个更通用的流程(大部分创建多使用这个方法),同时支持内置类型和自定义类型。 以创建浮点对象为例,我们还可以通过浮点类型 PyFloat_Type 来创建:例如
>>> pi = float('3.14')
>>> pi
3.14

2.对象的调用及多态性

(1)可调用对象:
  • 上面通过调用 类型对象的方式创出来的一个实例 pi, 这就说明对象是可调用的
  • 对象被调用时,执行的函数肯定是定义在了类型当中,那里保存着实例对象(类型对象之前所过,也是被type实例的 实例对象)的元信息
  • 所以再type中(PyType_Type):找到一个字段:tp_call
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    sizeof(PyHeapTypeObject),                   /* tp_basicsize */
    sizeof(PyMemberDef),                        /* tp_itemsize */

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

    // ...
};

我们知道了,当实例对象被调用时,便执行 tp_call 字段保存的处理函数。!这就是 python对象调用当中,底层C中的实现!!!

  • 因此 float(‘3.14’) 在 C 层面等价于:
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)
1. 首先浮点类型对象 ,调用ob_type字段对应的 类型: PyType_Type
2. 调用其中的tp_call, 完成对象的调用


即:
PyType_Type.tp_call(&PyFloat_Type, args, kwargs)


最终执行, type_call 函数:
type_call(&PyFloat_Type, args, kwargs)

书作者:调用参数通过 args 和 kwargs 两个对象传递,先不展开,留到函数机制中详细介绍。

  • 下面我们开始看type_call 函数,定义于 Include/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;
}
  • 不懂C语言没关系,只要能看懂大概:
    *(1)调用类型对象 tp_new 函数指针 申请内存 (第 7 行);
    • (2)必要时调用类型对象 tp_init 函数指针对对象进行 初始化 (第 15 行)
到这里对象的创建过程已经十分清晰了!

在这里插入图片描述
总结一下:

  • 调用 float (这里类型对象创建实例), Python 最终执行其类型对象 type 的 tp_call 函数;
  • tp_call 函数调用 float 的 tp_new 函数为实例对象分配 内存空间
  • tp_call 函数必要时进一步调用 tp_init 函数对实例对象进行 初始化
(2)对象的多态性实现
  • 首先,要实现多态性,首先要明白什么是多态!我找到一篇文章,里面例子介绍什么是多态,希望 认真观看:https://baijiahao.baidu.com/s?id=1646997242602110678&wfr=spider&for=pc
  • 面相对象程序设计的三大特性:封装性,继承性,多态性!
  • 我的理解,多态性,先从这个词理解就是:多种形态, 指的就是一个类型的属性或方法在子类中表现为不同的形态,既多种形态。就像上面的链接中的例子:多态性的应用实例, 我们三次执行时,调用的都是父类的同一个方法:construct(), 而传入的参数不同,父类会调用不同的 子类方法实现。
那么内部C语言层面上具体是如何进行实现多态性的呢?
  • 类似于上面所说的,Python 创建一个对象,比如 PyFloatObject ,会分配内存,并进行初始化。 此后, Python 内部统一通过一个 PyObject* 变量来保存和维护这个对象,而不是通过 PyFloatObject* 变量
  • 通过PyObject* 变量保存和维护对象,可以实现更抽象的上层逻辑,而不用关心对象的实际类型和实现细节。也就是泛型。抽象的,任何实际类型都可以传入,并完成对应的保存和维护。
  • 文中提到:以对象哈希值计算为例,假设有这样一个函数接口:
Py_hash_t
PyObject_Hash(PyObject *v);
// 该函数可以计算任意对象的哈希值,不管对象类型是啥。 例如,计算浮点对象哈希值:

PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Hash(fo);

// 对于其他类型,例如整数对象,也是一样的:
PyObject *lo = PyLongObject_FromLong(100);
PyObject_Hash(lo);
  • 可以看到上面的例子,一个函数,可以解决所有类型的对象求hash值的操作,答案在Object/object.c中
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);
}
  • 首先函数通过ob_type 指针找到对象的类型 (第 4 行)
  • 然后通过类型对象的 tp_hash 函数指针,调用对应的哈希值计算函数 (第 6 行)。
  • 也就是说: PyObject_Hash 根据对象的类型,调用不同的函数版本!! 这不就是多态吗?! 类似于上面链接中讲的例子,根据传入的实例对象不同,而调用对象的相对应方法完成操作! 这就是通过 ob_type 字段, Python 在 C 语言层面实现了对象的 多态 特性!
对象的行为
  • 我们上面的一节 通过提出两个问题说过,对象的 元数据保存在 类型对象中,并且规定了对象的 行为,既操作。
  • 不同对象的行为不同,比如哈希值计算方法就不同,由类型对象中 tp_hash 字段决定。 除了 tp_hash ,我们看到 PyTypeObject 结构体还定义了很多函数指针,这些指针最终都会指向某个函数,或者为空。 这些函数指针可以看做是 类型对象 中定义的 操作这些操作决定对应 实例对象 在运行时的 行为
  • 然而,除了对象特有的行为,不同对象还会有一些共性:比如 整数对象 和 浮点对象 都支持加减乘除等 数值型操作元组对象 tuple 和 列表对象 list 都支持下标索引操作
>>> 1 + 2
3
>>> 3.14 * 3.14
9.8596
  • 因此根据对象的行为,将对象进行分类:
    在这里插入图片描述
    (这也是很好的理解了,python中的对象分类!,受益匪浅,大彻大悟~)

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

    • PyNumberMethods 结构体定义了 数值型 操作
    • PySequenceMethods 结构体定义了 序列型 操作;
    • PyMappingMethods 结构体定义了 关联型 操作
  • 只要 类型对象 提供相关 操作集 , 实例对象 便具备对应的 行为!

  • 这里截取书中部分代码,展示例子:

// 以 float 为例,类型对象 PyFloat_Type 相关字段是这样初始化的:
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 */

    // ...
};
  • 解析:
    • 字段 tp_as_number 非空,因此 float 对象 支持数值型操作
    • 字段 tp_as_sequence 为空,因此 float 对象 不支持序列型操作
    • 字段 tp_as_mapping 为空,因此 float 对象 不支持关联型操作
引用计数

原话引入:
在这里插入图片描述
简单的例子(均引用书中例子):

>>> a = 3.14
>>> sys.getrefcount(a)
2
# 这里注意:对象作为函数参数传递,需要将引用计数加一,避免对象被提前销毁;
# 函数返回时,再将引用计数减一。 因此,例子中 getrefcount 函数看到的对象引用计数为 2 !!

# 接着,变量赋值让对象多了一个引用,这很好理解:
>>> b = a
>>> sys.getrefcount(a)
3

# 将对象放在容器对象中,引用计数也增加了,符合预期:
>>> l = [a]
>>> l
[3.14]
>>> sys.getrefcount(a)
4

# 我们将 b 变量删除,引用计数减少了:
>>> del b
>>> sys.getrefcount(a)
3

# 接着,将列表清空,引用计数进一步下降:

>>> l.clear()
>>> sys.getrefcount(a)
2

# 最后,将变量 a 删除后,引用计数降为 0 ,便不复存在了:

>>> del a
  • python中很多场景涉及到 应用计数的调整:
    • 容器操作
    • 变量赋值
    • 函数参数传递
    • 属性操作
  • 为此,Python 定义了两个非常重要的宏,用于维护对象应用计数。 其中, Py_INCREF 将对象应用计数加一 ( 3 行):
#define Py_INCREF(op) (                         \
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
    ((PyObject *)(op))->ob_refcnt++)
  • Py_DECREF 将引用计数减一 ( 5 行),并在引用计数为 0 时回收对象 ( 8 行):
#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 为一些常用对象维护了内存池, 对象回收后内存进入内存池中,以便下次使用,由此 避免频繁申请释放内存

内存池 技术作为程序开发的高级话题,需要更大的篇幅,放在后续章节中介绍。 让我们期待吧!,我也会持续学习,总结

总结:这一节我们学习了对象的生命周期。知道了如何创建一个对象,实现多态性,以及如何 规定一个对象所具有的行为,以及简单垃圾回收机制,理解对象的销毁。后面会深入的去讲解Python的内置类型,比如list,dict,str等! 期待一起学习,进步!

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐