您当前的位置:首页 > 文章 > 当调用一个 Python 对象时,背后都经历了哪些过程?

当调用一个 Python 对象时,背后都经历了哪些过程?

作者:古明地觉 时间:2024-05-24 阅读数:266 人阅读

楔子

在上一篇文章中,我们分析了对象是如何创建的,主要有两种方式,一种是通过特定类型 API,另一种是通过调用类型对象。

对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们既可以通过 [ ] 创建,也可以通过 list() 创建,前者是列表的特定类型 API,后者是调用类型对象。

但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。一个对象如果可以被调用,那么这个对象就是 callable,否则就不是 callable。而决定一个对象是不是 callable,则取决于它的类型对象。

  • 从 Python 的角度看,如果对象是 callable,那么它的类型对象一定实现了 __call__ 函数;
  • 从解释器的角度看,如果对象是 callable,那么它的类型对象的 tp_call 字段一定不为空。

从 Python 的角度看对象的调用

调用 int 可以创建一个整数,调用 str 可以创建一个字符串,调用 tuple 可以创建一个元组,调用自定义的类也可以创建出相应的实例对象,这就说明类型对象是可调用的,也就是 callable。

既然类型对象可调用,那么类型对象的类型对象(type)内部一定实现了 __call__ 函数。

# int 可以调用,那么它的类型对象、也就是元类(type)
# 内部一定实现了 __call__ 函数
print(hasattr(type, "__call__"))  # True

# 而调用一个对象,等价于调用其类型对象的 __call__ 函数
# 所以 int(2.71) 实际就等价于如下
print(type.__call__(int, 2.71))  # 2

我们说 int、str、float 这些都是类型对象(简单来说就是类),而 123、"你好"、2.71 是其对应的实例对象,这些都没问题。但相对 type 而言,int、str、float 是不是又成了实例对象呢?因为它们的类型是 type。

所以 class 具有二象性:

  • 如果站在实例对象(如:123、"satori"、2.71)的角度上,它是类型对象;
  • 如果站在 type 的角度上,它是实例对象;

同理,由于 type 的类型还是 type,那么 type 既是 type 的类型对象,type 也是 type 的实例对象。虽然这里描述的有一些绕,但应该不难理解,而为了避免后续的描述出现歧义,这里我们做一个申明:

  • 整数、浮点数、字符串、列表等等,我们称之为实例对象
  • int、float、str、dict,以及自定义的类,我们称之为类型对象
  • type 虽然也是类型对象,但我们称它为元类

由于 type 的内部定义了 __call__ 函数,那么说明类型对象都是可调用的,因为调用类型对象就是调用元类 type 的 __call__ 函数。而实例对象能否调用就不一定了,这取决于它的类型对象是否定义了 __call__ 函数,因为调用一个对象,本质上是调用其类型对象内部的 __call__ 函数。

class A:
    pass

a = A()
# 因为自定义的类 A 里面没有 __call__
# 所以 a 是不可以被调用的
try:
    a()
except Exception as e:
    # 告诉我们 A 的实例对象不可以被调用
    print(e)  # 'A' object is not callable

# 如果我们给 A 设置了一个 __call__
type.__setattr__(A, "__call__", lambda self: "这是__call__")
# 发现可以调用了
print(a())  # 这是__call__

这就是动态语言的特性,即便在类创建完毕之后,依旧可以通过 type 进行动态设置,而这在静态语言中是不支持的。所以 type 是所有类的元类,它控制了自定义类的生成过程,因此 type 这个古老而又强大的类可以让我们玩出很多新花样。

但对于内置的类,type 是不可以对其动态增加、删除或者修改属性的,因为内置的类在底层是静态定义好的。从源码中我们看到,这些内置的类、包括元类,它们都是 PyTypeObject 对象,在底层已经被声明为全局变量了,或者说它们已经作为静态类存在了。所以 type 虽然是所有类型对象的类型,但只有在面对我们自定义的类,type 才具有对属性进行增删改的能力。

而且在上一篇文章中我们也解释过,Python 的动态性是解释器将字节码翻译成 C 代码的时候动态赋予的,因此给类对象动态设置属性只适用于动态类,也就是在 py 文件中使用 class 关键字定义的类。

而对于静态类,它们在编译之后已经是指向 C 一级的数据结构了,不需要再被解释器解释了,因此解释器自然也就无法在它们身上动手脚,毕竟彪悍的人生不需要解释。

try:
    type.__setattr__(dict, "ping", "pong")
except Exception as e:
    print(e) 
    """
    cannot set 'ping' attribute of immutable type 'dict'
    """

try:
    type.__setattr__(list, "ping", "pong")
except Exception as e:
    print(e) 
    """
    cannot set 'ping' attribute of immutable type 'list'
    """

同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:

lst = list()
try:
    lst.name = "古明地觉"
except Exception as e:
    print(e)  # 'list' object has no attribute 'name'

在介绍 PyTypeObject 结构体的时候我们说过,静态类的实例对象可以绑定哪些属性,已经写死在 tp_members 字段里面了。

从解释器的角度看对象的调用

以内置类型 list 为例,我们说创建一个列表,可以通过 [ ] 或者 list() 的方式。前者使用列表的特定类型 API 创建,[ ] 会被直接解析成 C 一级的数据结构,也就是 PyListObject 实例;后者使用类型对象创建,对 list 进行调用,最终也得到指向 C 一级的数据结构 PyListObject 实例。

第一种方式我们已经很熟悉了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可,因为解释器对内置的数据结构了如指掌。我们重点来看第二种方式,也就是通过调用类型对象去创建实例对象。

如果一个对象可以被调用,那么它的类型对象中一定要有 tp_call,更准确的说是 tp_call 字段的值是一个具体的函数指针,而不是 0。由于 PyList_Type 是可以调用的,这就说明 PyType_Type 内部的 tp_call 是一个函数指针,这在 Python 的层面我们已经验证过了,下面再来通过源码看一下。

图片图片

在创建 PyType_Type 的时候,PyTypeObject 内部的 tp_call 字段被设置成了 type_call。所以当我们调用 PyList_Type 的时候,会执行 type_call 函数。

因此 list() 在 C 的层面上等价于:

(&PyList_Type)->ob_type->tp_call(&PyList_Type, args, kwargs);
// 即:
(&PyType_Type)->tp_call(&PyList_Type, args, kwargs);
// 而在创建 PyType_Type 的时候,给 tp_call 字段传递的是 type_call
// 因此最终相当于
type_call(&PyList_Type, args, kwargs)

如果用 Python 来演示这一过程的话:

# 以 list("abcd") 为例,它等价于
lst1 = list.__class__.__call__(list, "abcd")
# 等价于
lst2 = type.__call__(list, "abcd")
print(lst1)  # ['a', 'b', 'c', 'd']
print(lst2)  # ['a', 'b', 'c', 'd']

这就是 list() 的秘密,相信其它类型在实例化的时候是怎么做的,你已经知道了,做法是相同的。

# dct = dict([("name", "古明地觉"), ("age", 17)])
dct = dict.__class__.__call__(
    dict, [("name", "古明地觉"), ("age", 17)]
)
print(dct)  # {'name': '古明地觉', 'age': 17}

# buf = bytes("hello world", encoding="utf-8")
buf = bytes.__class__.__call__(
    bytes, "hello world", encoding="utf-8"
)
print(buf)  # b'hello world'

当然,目前还没有结束,我们还需要看一下 type_call 的源码实现。

type_call 源码解析

调用类型对象,本质上会调用 type.__call__,在底层对应 type_call 函数,因为 PyType_Type 的 tp_call 字段被设置成了 type_call。当然调用 type 也是如此,因为 type 的类型还是 type。

那么这个 type_call 都做了哪些事情呢?

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{   
    // 参数 type 表示类型对象或者元类,假设调用的是 list,那么它就是 &PyList_Type
    // 参数 args 和 kwds 表示位置参数和关键字参数,args 是元组,kwds 是字典

    // 创建的实例对象,当然也可能是类型对象,取决于参数 type
    PyObject *obj;  
    // 线程状态对象,后续介绍线程的时候会细说
    // 此处的线程状态对象是用来设置异常的
    PyThreadState *tstate = _PyThreadState_GET();

    // 如果参数 type 是 &PyType_Type,也就是 Python 中的元类
    if (type == &PyType_Type) {
        // 那么它只能接收一个位置参数(查看对象类型)或三个位置参数(动态创建类)
        Py_ssize_t nargs = PyTuple_GET_SIZE(args);  // 获取位置参数的个数
        // 如果位置参数个数为 1,并且没有传递关键字参数,那么直接返回对象的类型
        if (nargs == 1 && (kwds == NULL || !PyDict_GET_SIZE(kwds))) {
            // Py_TYPE 负责获取对象类型,因此相当于 type(args[0])
            obj = (PyObject *) Py_TYPE(PyTuple_GET_ITEM(args, 0));
            // 增加引用计数,返回 obj
            return Py_NewRef(obj);
        }

        // 如果位置参数的个数不等于 1,那么一定等于 3
        if (nargs != 3) {
            PyErr_SetString(PyExc_TypeError,
                            "type() takes 1 or 3 arguments");
            return NULL;
        }
    }
    // 接下来执行类型对象(也可能是元类)的 tp_new,也就是 __new__
    // 如果不存在,那么会报错,而在 Python 中见到的报错信息就是这里指定的
    if (type->tp_new == NULL) {
        _PyErr_Format(tstate, PyExc_TypeError,
                      "cannot create '%s' instances", type->tp_name);
        return NULL;
    }
    // 执行类型对象的 __new__
    obj = type->tp_new(type, args, kwds);
    // 检测调用是否正常,如果调用正常,那么 obj 一定指向一个合法的 PyObject
    // 而如果 obj 为 NULL,则表示执行出错,此时解释器会抛出异常
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    // __new__ 执行完之后该执行啥了,显然是 __init__,但需要先做一个检测
    // 如果 __new__ 返回的实例对象的类型不是当前类型,那么直接返回,不再执行 __init__
    // 比如自定义 class A,那么在 __new__ 里面应该返回 A 的实例对象,但假设返回个 123
    // 由于返回值的类型不是当前类型,那么不再执行初始化函数 __init__
    if (!PyObject_TypeCheck(obj, type))
        return obj;
    // 走到这里说明类型一致,那么执行 __init__,将 obj、args、kwds 一起传过去
    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_SETREF(obj, NULL);
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    // 返回创建的对象 obj
    return obj;
}

所以整个过程就三步:

  • 如果传递的是元类,并且只有一个参数,那么直接返回对象的类型;
  • 否则先调用 tp_new 为实例对象申请内存;
  • 再调用 tp_init(如果有)进行初始化,设置对象属性;

所以这对应了 Python 中的 __new__ 和 __init__,其中 __new__ 负责为实例对象开辟一份内存,然后返回指向对象的指针,并且该指针会自动传递给 __init__ 中的 self。

class Girl:

    def __new__(cls, name, age):
        print("__new__ 方法执行啦")
        # 调用 object.__new__(cls) 创建 Girl 的实例对象
        # 然后该对象的指针会自动传递给 __init__ 中的 self
        return object.__new__(cls)

    def __init__(self, name, age):
        print("__init__ 方法执行啦")
        self.name = name
        self.age = age


g = Girl("古明地觉", 16)
print(g.name, g.age)
"""
__new__ 方法执行啦
__init__ 方法执行啦
古明地觉 16
"""

__new__ 里面的参数要和 __init__ 里面的参数保持一致,因为会先执行 __new__,然后解释器再将 __new__ 的返回值和传递的参数组合起来一起传给 __init__。因此从这个角度讲,设置属性完全可以在 __new__ 里面完成。

class Girl:

    def __new__(cls, name, age):
        self = object.__new__(cls)
        self.name = name
        self.age = age
        return self


g = Girl("古明地觉", 16)
print(g.name, g.age)
"""
古明地觉 16
"""

这样也是没问题的,不过 __new__ 一般只负责创建实例,设置属性应该交给 __init__ 来做,毕竟一个是构造函数、一个是初始化函数,各司其职。另外由于 __new__ 里面不负责初始化,那么它的参数除了 cls 之外,一般都会写成 *args 和 **kwargs。

然后再回过头来看一下 type_call 中的这两行代码:

图片图片

tp_new 应该返回该类型对象的实例对象,而且一般情况下我们是不重写 __new__ 的,会默认执行 object 的 __new__。但如果我们重写了,那么必须要手动返回 object.__new__(cls)。可如果我们不返回,或者返回其它的话,会怎么样呢?

class Girl:

    def __new__(cls, *args, **kwargs):
        print("__new__ 方法执行啦")
        instance = object.__new__(cls)
        # 打印看看 instance 到底是个啥
        print("instance:", instance)
        print("type(instance):", type(instance))

        # 正确做法是将 instance 返回
        # 但是我们不返回,而是返回一个整数 123
        return 123

    def __init__(self, name, age):
        print("__init__ 方法执行啦")


g = Girl()
"""
__new__ 方法执行啦
instance: <__main__.Girl object at 0x0000019A2B7270A0>
type(instance): <class '__main__.Girl'>
"""

这里面有很多可以说的点,首先就是 __init__ 里面需要两个参数,但是我们没有传,却还不报错。原因就在于这个 __init__ 压根就没有执行,因为 __new__ 返回的不是 Girl 的实例对象。

通过打印 instance,我们知道了 object.__new__(cls) 返回的就是 cls 的实例对象,而这里的 cls 就是 Girl 这个类本身。所以我们必须要返回 instance,才会自动执行相应的 __init__。

我们在外部来打印一下创建的实例对象吧,看看结果:

class Girl:

    def __new__(cls, *args, **kwargs):
        return 123

    def __init__(self, name, age):
        print("__init__ 方法执行啦")


g = Girl()
print(g)
"""
123
"""

我们看到打印的结果是 123,所以再次总结一下 tp_new 和 tp_init 之间的区别,当然也对应 __new__ 和 __init__ 的区别:

  • tp_new:为实例对象申请内存,底层会调用 tp_alloc,至于对象的大小则记录在 tp_basicsize 字段中,而在 Python 里面则是调用 object.__new__(cls),然后返回;
  • tp_init:tp_new 的返回值会自动传递给 self,然后为 self 绑定相应的属性,也就是进行实例对象的初始化;

但如果 tp_new 返回的对象的类型不对,比如 type_call 的第一个参数接收的是 &PyList_Type,但 tp_new 返回的却是 PyTupleObject *,那么此时就不会执行 tp_init。

对应上面的 Python 代码就是,Girl 的 __new__ 应该返回 Girl 的实例对象(指针)才对,但却返回了整数,因此类型不一致,不会执行 __init__。

所以都说类在实例化的时候会先调用 __new__,再调用 __init__,相信你应该知道原因了,因为在源码中先调用 tp_new,再调用 tp_init。所以源码层面表现出来的,和我们在 Python 层面看到的是一样的。

小结

到此,我们就从 Python 和解释器两个层面解释了对象是如何调用的,更准确的说我们是从解释器的角度对 Python 层面的知识进行了验证,通过 tp_new 和 tp_init 的关系,来了解 __new__ 和 __init__ 的关系。

当然对象调用还不止目前说的这么简单,更多的细节隐藏在了幕后。后续我们会循序渐进,一点点地揭开它的面纱,并且在这个过程中还会不断地学习到新的东西。比如说,实例对象在调用方法的时候会自动将实例本身作为参数传递给 self,那么它为什么会传递呢?解释器在背后又做了什么工作呢?这些在之后的文章中都会详细说明。

本站大部分文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了您的权益请来信告知我们删除。邮箱:1451803763@qq.com