Python对象的生命周期源码学习

目录
  • 思考:
  • 1 C API
  • 2 对象的创建
    • 2.1 两种创建对象的方式
    • 2.2 由类型对象创建实例对象
  • 3 对象的多态性
  • 4 对象的行为
  • 5 引用计数

思考:

当我们输入这个语句的时候,Python内部是如何去创建这个对象的?

a = 1.0

对象使用完毕,销毁的时机又是怎么确定的呢?

下面,我们以一个基本类型float为例,来分析对象从创建到销毁这整个生命周期中的行为。

1 C API

Python是用C写的,对外提供了API,让用户可以从C环境中与其交互,并且Python内部也大量使用了这些API。C API分为两类:泛型API以及特型API。

泛型API:与类型无关,属于抽象对象层,这类API的参数是PyObject *,即可以处理任意类型的对象。以PyObject_Print为例:

// 打印浮点对象
PyObject *fo = PyFloat_FromDouble(3.14);
PyObject_Print(fo, stdout, 0);
// 打印整数对象
PyObject *lo = PyLong_FromLong(100);
PyObject_Print(lo, stdout, 0);

特型API:与类型相关,属于具体对象层,这类API只能作用于某种类型的对象

2 对象的创建

2.1 两种创建对象的方式

Python内部一般通过两种方法创建对象:

通过C API,多用于内建类型

以浮点类型为例,Python内部提供PyFloat_FromDouble,这是一个特型C API,在这个接口内部为PyFloatObject结构体变量分配内存,并初始化相关字段:

PyObject *
PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        free_list = (PyFloatObject *) Py_TYPE(op);
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}

通过类型对象,多用于自定义类型

对于自定义类型,Python就无法事先提供C API了,这种情况下就只能通过类型对象中包含的元数据(分配多少内存,如何初始化等等)来创建实例对象。

由类型对象创建实例对象是一个更通用的流程,对于内建类型,除了通过C API来创建对象意外,同样也可以通过类型对象来创建。以浮点类型为例,我们通过类型对象float,创建了一个实例对象f:

f: float = float('3.123')

2.2 由类型对象创建实例对象

思考:既然我们可以通过类型对象来创建实例对象,那么类型对象中应该存在相应的接口。

在PyType_Type中找到了tp_call字段:

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 */
    // ...
    (ternaryfunc)type_call,                     /* tp_call */
    // ...
};

因此,float(‘3.123’)在C层面就等价于:

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

这里大家可以思考下为什么是PyFloat_Type.ob_type——因为我们在float(‘3.14’)中是通过float这个类型对象去创建一个浮点对象,而对象的通用方法是由它对应的类型管理的,自然float的类型就是type,所以我们要找的就是type的tp_call字段。

type_call函数的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;
}

其中有两个关键的步骤:(这两个步骤大家应该是很熟悉的)

  • 调用类型对象的tp_new函数指针,用于申请内存;
  • 如果类型对象的tp_init函数指针不为空,则会对对象进行初始化。

总结:(以float为例)

  • 调用float,Python最终执行的是其类型对象type的tp_call指针指向的type_call函数。
  • type_call函数调用float的tp_new函数为实例对象分配内存空间。
  • type_call函数必要时进一步调用tp_init函数对实例对象进行初始化。

图示如下:

3 对象的多态性

通过类型对象创建实例对象,最后会落实到调用type_call函数,其中保存具体对象时,使用的是PyObject *obj,并没有通过一个具体的对象(例如PyFloatObject)来保存。这样做的好处是:可以实现更抽象的上层逻辑,而不用关心对象的实际类型和实现细节。(记得当初从C语言的面向过程向Java中的面向对象过度的时候,应该就是从结构体)

以对象哈希值计算为例,有这样一个函数接口:

Py_hash_t
PyObject_Hash(PyObject *v)
{
    // ...
}

对于浮点数对象和整数对象:

PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Hash(fo);
PyObject *lo = PyLongObject_FromLong(100);
PyObject_Hash(lo);

可以看到,对于浮点数对象和整数对象,我们计算对象的哈希值时,调用的都是PyObject_Hash()这个函数,但是对象类型不同,其行为是有区别的,哈希值计算也是如此。

那么在PyObject_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函数指针来调用对应的哈希计算函数。

即:PyObject_Hash()函数根据对象的类型,调用不同的函数版本,这就是多态。

4 对象的行为

除了tp_hash字段,PyTypeObject结构体还定义了很多函数指针,这些指针最终都会指向某个函数,或者为空。我们可以把这些函数指针看作是类型对象中定义的操作,这些操作决定了对应的实例对象在运行时的行为。

虽然不同的类型对象中保存了对应实例对象共有的行为,但是不同类型的对象也会存在一些共性。例如:整数对象和浮点数对象都支持加减乘除等擦欧总,元组对象和列表对象都支持下标操作。因此,我们以行为为分类标准,对对象进行分类:

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 */
   // ...
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;
    // ...
} PyTypeObject;

以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对象支持数值型操作,不支持序列型操作和关联型操作。

5 引用计数

在Python中,很多场景都涉及引用计数的调整:

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

引用计数是Python生命周期中很关键的一个知识点,后续我会用一个单独的章节来介绍,这里咱们先按下不表,更多关于Python对象生命周期的资料请关注我们其它相关文章!

(0)

相关推荐

  • Python中for循环可迭代对象迭代器及生成器源码学习

    目录 问题: 1. 迭代 1.1 可迭代对象Iterable 1.2 迭代器Iterator 1.3 for循环 1.3.1 iter()方法和next()方法 1.3.2 iter()和__iter__() 1.3.3 next()和__next__() 1.3.4 自定义类实现__iter__()和__next__() 1.3.5 探究for循环 2 生成器 2.1 获取生成器 2.2 next(生成器) 2.3 生成器和迭代器 2.4 生成器解析式 问题: 之前在学习list和dict相关

  • Python万物皆对象理解及源码学习

    目录 万物皆对象 1 类型对象和实例对象 2 类型.对象体系 2.1 元类型type 2.2 自定义类型 2.3 自定义类型子类 2.4 type和object的关系 3 可变对象与不可变对象 4 变长对象和定长对象 5 补充 万物皆对象 这篇博客的内容主要是针对Python中万物皆对象的理解,对Python的类型.对象体系做一个整体的梳理. 在Python中,一切皆为对象,一个整数是一个对象,一个字符串也是一个对象,基本类型(如int)也是对象.Python不再区别对待基本类型和对象,所有的基

  • Python学习之面向对象编程详解

    目录 什么是面向对象编程(类) 类的关键字-class 类的定义与使用 类的参数-self self的解析与总结 类的构造函数 构造函数的创建方法 关于对象的生命周期 什么是面向对象编程(类) 利用(面向)对象的(属性和方法)去进行编码的过程即面向对象编程 自定义对象数据类型就是面向对象中的类(class)的概念 类的关键字 - class class 关键字用来声明类,类的名称首字母大写,多单词的情况下每个单词首字母大写(即驼峰命名法).在我们一开始学习 Python 的时候说过,要尽量避免使

  • Python浅复制中对象生存周期实例分析

    本文实例讲述了Python浅复制中对象生存周期.分享给大家供大家参考,具体如下: Python的浅复制以及深复制我了解的并不多,我不清楚shutil中的copy1和copy2是不是算是浅复制和深复制?这两个操作,一个是文件属性重建,还有一个是文件属性不变.不知道对象的复制是否可以以此类比? 在复制之前,为了能够观看一下GC的执行,定义一个特殊的类和对象.测试代码如下: class Demo: def __del__(self): print("Removing{0}".format(i

  • Python对象的生命周期源码学习

    目录 思考: 1 C API 2 对象的创建 2.1 两种创建对象的方式 2.2 由类型对象创建实例对象 3 对象的多态性 4 对象的行为 5 引用计数 思考: 当我们输入这个语句的时候,Python内部是如何去创建这个对象的? a = 1.0 对象使用完毕,销毁的时机又是怎么确定的呢? 下面,我们以一个基本类型float为例,来分析对象从创建到销毁这整个生命周期中的行为. 1 C API Python是用C写的,对外提供了API,让用户可以从C环境中与其交互,并且Python内部也大量使用了这

  • Python对象的底层实现源码学习

    目录 1. PyObject:对象的基石 2. PyVarObject:变长对象的基础 2.1 浮点对象 2.2 列表对象 3. PyTypeObject:类型的基石 4. PyType_Type:类型的类型 5. PyBaseObject_Type:类型之基 6. 补充 在“Python源码学习笔记:Python万物皆对象”中,我们对Python的对象类型体系有了一定的认识,这篇博客将从源码层面来介绍Python中万物皆对象的底层实现. 1. PyObject:对象的基石 在Python解释器

  • Python内建类型float源码学习

    目录 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type 1.3 对象的创建 1.4 对象的销毁 1.5 小结 2 空闲对象缓存池 2.1 浮点对象的空闲链表 2.2 空闲链表的使用 3 其他 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type C源码(仅列出部分字段): PyTypeObject P

  • Python内建类型int源码学习

    目录 1 int对象的设计 1.1 PyLongObject 1.2 整数的布局 1.3 小整数静态对象池 1.4 示例 2 大整数运算 2.1 整数运算概述 2.2 大整数运算处理过程 1.long_add()源码: 2.绝对值加法x_add() 3 其他 大整数转float溢出 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 问题:对于C语言,下面这个程序运行后的结果是什么?是1000000000000吗? #include <stdio

  • Python内建类型str源码学习

    目录 引言 1 Unicode 2 Python中的Unicode 2.1 Unicode对象的好处 2.2 Python对Unicode的优化 3 Unicode对象的底层结构体 3.1 PyASCIIObject 3.2 PyCompactUnicodeObject 3.3 PyUnicodeObject 3.4 示例 4 interned机制 5 总结 引言 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 在介绍常用类型str之前,在上

  • Python作用域与名字空间源码学习笔记

    目录 作用域与名字空间 1. 名字绑定 1.1 赋值 1.2 模块导入 1.3 函数.类定义 1.4 as关键字 2. 作用域 2.1 静态作用域 2.2 划分作用域 2.3 闭包作用域 2.4 类作用域 2.5 复杂嵌套 2.5.1 函数嵌套类 2.5.2 类嵌套类 3. 名字空间 3.1 Globals 3.2 Locals 3.3 Enclosings 3.4 Builtin 4. 问题与总结 作用域与名字空间 问题: PI = 3.14 def circle_area(r): retur

  • Python内建类型list源码学习

    目录 问题: 1 常用方法 小结: 题外话: 2 list的内部结构:PyListObject 3 尾部操作和头部操作 3.1 尾部操作 3.2 头部操作 4 浅拷贝和深拷贝 4.1 浅拷贝 4.2 深拷贝 4.3 直接赋值 4.4 小结 个人总结: TODO: 5 动态数组 5.1 容量调整 5.2 append() 5.3 insert() 5.4 pop() 5.5 remove() 6 一些问题 问题: “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种

  • Spring Bean生命周期源码原理图解

    概述 spring流行了很长时间了,是一个用java编写的轻量级框架,受到了很多公司及程序员的欢迎,Bean生命周期是一个对象从实例化开始到销毁的全过程,了解生命周期是很有必要的. 重要性 spring的生命周期是比较复杂的,只有了解其过程及原理才能更好的扩展程序. 源码剖析生命周期过程bean的实例化 属性填充及Aware接口检测设置依赖 如果容器自定义了BeanpostProcessor的实现类,则执行处理器相应的前置处理 bean实现了初始化接口InitlializingBean,则会执行

随机推荐