> 백엔드 개발 > 파이썬 튜토리얼 > 소스 코드 탐색: Python에서 객체는 어떻게 호출되나요?

소스 코드 탐색: Python에서 객체는 어떻게 호출되나요?

PHPz
풀어 주다: 2023-05-11 11:46:05
앞으로
1389명이 탐색했습니다.

源码探秘:Python 中对象是如何被调用的?

Wedge

우리는 객체가 두 가지 주요 방법으로 생성된다는 것을 알고 있습니다. 하나는 Python/C API를 사용하는 것이고, 다른 하나는 유형 객체를 호출하는 것입니다. 내장 유형의 인스턴스 객체의 경우 두 가지 방법이 모두 지원됩니다. 예를 들어 목록은 [] 또는 list()를 통해 생성할 수 있습니다. 전자는 Python/C API이고 후자는 호출 유형 객체입니다.

그러나 사용자 정의 클래스의 인스턴스 객체의 경우 유형 객체를 호출해야만 생성할 수 있습니다. 객체를 호출할 수 있으면 해당 객체는 호출 가능하고, 그렇지 않으면 호출할 수 없습니다.

객체가 호출 가능한지 여부는 해당 유형 객체에 메서드가 정의되어 있는지 여부에 따라 결정됩니다. Python 관점에서 보면 이 메소드는 __call__ 이고, 인터프리터 관점에서 보면 이 메소드는 tp_call 입니다.

Python 관점에서 객체 호출을 보면

Call int, str, tuple은 정수, 문자열, tuple을 생성할 수 있고, 사용자 정의 클래스를 호출하면 해당 인스턴스 객체도 생성할 수 있습니다. 이는 유형 객체가 호출 가능함을 나타냅니다. , 호출 가능. 그런 다음 이러한 유형 객체(int, str, tuple, class 등)의 유형 객체(type) 내부에 __call__ 메서드가 있어야 합니다.

# int可以调用
# 那么它的类型对象、也就是元类(type), 内部一定有__call__方法
print(hasattr(type, "__call__"))# True
# 而调用一个对象,等价于调用其类型对象的 __call__ 方法
# 所以 int(3.14)实际就等价于如下
print(type.__call__(int, 3.14))# 3
로그인 후 복사

참고: 여기 설명은 약간 혼란스러울 수 있습니다. int, str 및 float는 모두 유형 개체(간단히 말하면 클래스)이고 123, "Hello" 및 3.14는 해당 인스턴스 개체입니다. 괜찮아요. 그러나 유형이 유형 객체입니까? 당연히 그렇습니다. 비록 우리가 그것을 메타클래스라고 부르지만, 그것은 또한 유형 객체이기도 합니다. print(type)이 클래스를 표시한다면, 그것은 또한 유형 객체입니다.

그럼 type에 비해 int, str, float이 다시 인스턴스 객체가 되는 건가요? 그들의 유형은 유형이기 때문입니다.

그래서 클래스에는 이중성이 있습니다.

  • 인스턴스 개체(예: 123, "satori", [], 3.14)의 관점에서 보면 유형 개체입니다.
  • 관점에서 보면 유형 개체입니다. 유형의 경우 인스턴스 객체입니다

마찬가지로 유형의 유형도 유형이므로 유형은 둘 다 유형의 유형 객체이면서 유형도 유형의 인스턴스 객체입니다. 여기 설명은 다소 혼란스러울 수 있지만 이해하기 어렵지 않아야 하며, 이후 설명에서 모호함을 피하기 위해 다음과 같이 설명합니다.

  • 정수, 부동 소수점 숫자, 문자열 등 인스턴스 객체라고 부릅니다.
  • int, float, str, dict 및 사용자 정의 클래스를 유형 객체라고 합니다.
  • 유형도 유형 객체이지만 우리는 이를 메타클래스라고 부릅니다.

그래서 유형의 내부는 _call__ 메소드인 경우 이는 유형 객체를 호출하는 것이 유형의 __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__
로그인 후 복사

이것이 동적 언어의 특징임을 알 수 있습니다. 클래스가 생성된 후에도 정적 언어에서는 지원되지 않는 유형을 통해 동적으로 설정할 수 있습니다. 따라서 유형은 모든 클래스의 메타클래스입니다. 이는 사용자 정의 클래스의 생성 프로세스를 제어합니다. 이 오래되고 강력한 클래스 유형을 사용하면 많은 새로운 트릭을 사용할 수 있습니다.

그러나 내장 클래스의 경우 내장 클래스가 최하위 레이어에 정적으로 정의되기 때문에 유형은 속성을 동적으로 추가, 삭제 또는 수정할 수 없습니다. 소스 코드에서 메타클래스를 포함한 이러한 내장 클래스가 모두 PyTypeObject 객체이고 최하위 수준에서 전역 변수로 선언되었거나 이미 정적 클래스로 존재한다는 것을 볼 수 있기 때문입니다. 따라서 type은 모든 유형의 객체에 대한 메타클래스이지만 사용자 정의 클래스에 대해서는 추가, 삭제 및 수정 기능만 갖습니다.

또한 인터프리터가 바이트코드를 C 코드로 변환할 때 Python의 동적 특성이 동적으로 할당된다는 점을 설명했습니다. 따라서 클래스의 속성이나 메서드를 동적으로 설정하는 것은 동적 클래스, 즉 py 클래스에만 적용됩니다. class 키워드를 사용하여 정의됩니다.

확장 모듈을 작성할 때 정의된 정적 클래스 또는 확장 클래스의 경우(둘은 동일함) 컴파일 후 이미 C 수준 데이터 구조를 가리키며 인터프리터가 해석할 필요가 없으므로 인터프리터는 자연스럽게 변조할 수 없습니다. 결국 강한 삶은 설명이 필요하지 않습니다.

try:
 type.__setattr__(dict, "__call__", lambda self: "这是__call__")
except Exception as e:
 print(e)# can't set attributes of built-in/extension type 'dict'
로그인 후 복사

내장/확장 유형 dict가 인터프리터 해석 및 실행 단계를 우회하여 해당 속성을 동적으로 설정할 수 없기 때문에 속성을 설정할 수 없다는 예외가 발생하는 것을 확인했습니다.

정적 클래스의 인스턴스 객체는 속성을 동적으로 설정할 수 없습니다.

class Girl:
 pass
g = Girl()
g.name = "古明地觉"
# 实例对象我们也可以手动设置属性
print(g.name)# 古明地觉
lst = list()
try:
 lst.name = "古明地觉"
except Exception as e:
 # 但是内置类型的实例对象是不可以的
 print(e)# 'list' object has no attribute 'name'
로그인 후 복사

어떤 사람들은 왜 나열할 수 없는지 궁금해할 수도 있습니다. 이에 대한 대답은 내장형의 인스턴스 객체에는 __dict__ 속성 ​​사전이 없다는 것입니다. 관련 속성이나 메소드가 맨 아래에 정의되어 있어 동적으로 추가할 수 없기 때문입니다. 클래스를 커스터마이즈할 때 __slots__를 설정하면 내장 클래스와 효과가 동일합니다.

当然了,我们后面会介绍如何通过动态修改解释器来改变这一点,举个栗子,不是说静态类无法动态设置属性吗?下面我就来打自己脸:

import gc
try:
 type.__setattr__(list, "ping", "pong")
except TypeError as e:
 print(e)# can't set attributes of built-in/extension type 'list'
# 我们看到无法设置,那么我们就来改变这一点
attrs = gc.get_referents(tuple.__dict__)[0]
attrs["ping"] = "pong"
print(().ping)# pong
attrs["append"] = lambda self, item: self + (item,)
print(
 ().append(1).append(2).append(3)
)# (1, 2, 3)
로그인 후 복사

我脸肿了。好吧,其实这只是我们玩的一个小把戏,当我们介绍完整个 CPython 的时候,会来专门聊一聊如何动态修改解释器。比如:让元组变得可修改,让 Python 真正利用多核等等。

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

我们以内置类型 float 为例,我们说创建一个 PyFloatObject,可以通过3.14或者float(3.14)的方式。前者使用Python/C API创建,3.14直接被解析为 C 一级数据结构,也就是PyFloatObject实例;后者使用类型对象创建,通过对float进行一个调用、将3.14作为参数,最终也得到指向C一级数据结构PyFloatObject实例。

Python/C API的创建方式我们已经很清晰了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可。我们重点看一下通过类型调用来创建实例对象的方式。

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

//typeobject.c
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 */
 //... /* tp_hash */
 (ternaryfunc)type_call, /* tp_call */
 //...
}
로그인 후 복사

我们看到在实例化PyType_Type的时候PyTypeObject内部的成员tp_call被设置成了type_call。这是一个函数指针,当我们调用PyFloat_Type的时候,会触发这个type_call指向的函数。

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

(&PyFloat_Type) -> ob_type -> tp_call(&PyFloat_Type, args, kwargs);
// 即:
(&PyType_Type) -> tp_call(&PyFloat_Type, args, kwargs);
// 而在创建 PyType_Type 的时候,给 tp_call 成员传递的是 type_call
// 因此最终相当于
type_call(&PyFloat_Type, args, kwargs)
로그인 후 복사

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

# float(3.14),等价于
f1 = float.__class__.__call__(float, 3.14)
# 等价于
f2 = type.__call__(float, 3.14)
print(f1, f2)# 3.14 3.14
로그인 후 복사

这就是 float(3.14) 的秘密,相信list、dict在实例化的时候是怎么做的,你已经猜到了,做法是相同的。

# lst = list("abcd")
lst = list.__class__.__call__(list, "abcd")
print(lst)# ['a', 'b', 'c', 'd']
# dct = dict([("name", "古明地觉"), ("age", 17)])
dct = dict.__class__.__call__(dict, [("name", "古明地觉"), ("age", 17)])
print(dct)# {'name': '古明地觉', 'age': 17}
로그인 후 복사

最后我们来围观一下 type_call 函数,我们说 type 的 __call__ 方法,在底层对应的是 type_call 函数,它位于Object/typeobject.c中。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 // 如果我们调用的是 float
 // 那么显然这里的 type 就是 &PyFloat_Type

 // 这里是声明一个PyObject *
 // 显然它是要返回的实例对象的指针
 PyObject *obj;

 // 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了
 // 我们说__call__对应底层的tp_call
 // 显然__new__对应底层的tp_new,这里是为实例对象分配空间
 if (type->tp_new == NULL) {
 // tp_new 是一个函数指针,指向具体的构造函数
 // 如果 tp_new 为空,说明它没有构造函数
 // 因此会报错,表示无法创建其实例
 PyErr_Format(PyExc_TypeError,
"cannot create '%.100s' instances",
type->tp_name);
 return NULL;
 }

 //通过tp_new分配空间
 //此时实例对象就已经创建完毕了,这里会返回其指针
 obj = type->tp_new(type, args, kwds);
 //类型检测,暂时不用管
 obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
 if (obj == NULL)
 return NULL;
 //我们说这里的参数type是类型对象,但也可以是元类
 //元类也是由PyTypeObject结构体实例化得到的
 //元类在调用的时候执行的依旧是type_call
 //所以这里是检测type指向的是不是PyType_Type
 //如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象
 //要单独检测一下
 if (type == &PyType_Type &&
 PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
 (kwds == NULL ||
(PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
 return obj;
 //tp_new应该返回相应类型对象的实例对象(的指针)
 //但如果不是,就直接将这里的obj返回
 //此处这么做可能有点难理解,我们一会细说
 if (!PyType_IsSubtype(Py_TYPE(obj), type))
 return obj;

 //拿到obj的类型
 type = Py_TYPE(obj);
 //执行 tp_init
 //显然这个tp_init就是__init__函数
 //这与Python中类的实例化过程是一致的。
 if (type->tp_init != NULL) {
 //将tp_new返回的对象作为self,执行 tp_init
 int res = type->tp_init(obj, args, kwds);
 if (res < 0) {
 //执行失败,将引入计数减1,然后将obj设置为NULL
 assert(PyErr_Occurred());
 Py_DECREF(obj);
 obj = NULL;
 }
 else {
 assert(!PyErr_Occurred());
 }
 }
 //返回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的实例对象
 # 因此这里的cls指的就是这里的Girl,注意:一定要返回
 # 因为__new__会将自己的返回值交给__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__里面的参数除了cls之外,一般都会写*args和**kwargs。

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

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 //......
 //......
 if (!PyType_IsSubtype(Py_TYPE(obj), type))
 return obj;

 //......
 //......
}
로그인 후 복사

我们说tp_new应该返回该类型对象的实例对象,而且一般情况下我们是不写__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 0x000002C0F16FA1F0>
type(instance): <class '__main__.Girl'>
"""
로그인 후 복사

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

通过打印 instance,我们知道了object.__new__(cls) 返回的就是 cls 的实例对象,而这里的cls就是Girl这个类本身。我们必须要返回instance,才会执行对应的__init__,否则__new__直接就返回了。我们在外部来打印一下创建的实例对象吧,看看结果:

class Girl:
 def __new__(cls, *args, **kwargs):
 return 123
 def __init__(self, name, age):
 print("__init__方法执行啦")
g = Girl()
print(g, type(g))# 123 <class 'int'>
로그인 후 복사

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

  • tp_new:为该类型对象的实例对象申请内存,在Python的__new__方法中通过object.__new__(cls)的方式申请,然后将其返回
  • tp_init:tp_new的返回值会自动传递给self,然后为self绑定相应的属性,也就是进行实例对象的初始化

但如果tp_new返回的不是对应类型的实例对象的指针,比如type_call中第一个参数接收的&PyFloat_Type,但是tp_new中返回的却是PyLongObject *,所以此时就不会执行tp_init。

以上面的代码为例,我们Girl中的__new__应该返回Girl的实例对象才对,但实际上返回了整型,因此类型不一致,所以不会执行__init__。

下面我们可以做总结了,通过类型对象去创建实例对象的整体流程如下:

  • 第一步:获取类型对象的类型对象,说白了就是元类,执行元类的 tp_call 指向的函数,即 type_call
  • 第二步:type_call 会调用该类型对象的 tp_new 指向的函数,如果 tp_new 为 NULL,那么会到 tp_base 指定的父类里面去寻找 tp_new。在新式类当中,所有的类都继承自 object,因此最终会执行 object 的 __new__。然后通过访问对应类型对象中的 tp_basicsize 信息,这个信息记录着该对象的实例对象需要占用多大的内存,继而完成申请内存的操作
  • 调用type_new 创建完对象之后,就会进行实例对象的初始化,会将指向这片空间的指针交给 tp_init,但前提是 tp_new 返回的实例对象的类型要一致。

所以都说 Python 在实例化的时候会先调用 __new__ 方法,再调用 __init__ 方法,相信你应该知道原因了,因为在源码中先调用 tp_new、再调用的 tp_init。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 //调用__new__方法, 拿到其返回值
 obj = type->tp_new(type, args, kwds);
 if (type->tp_init != NULL) {
 //将__new__返回的实例obj,和args、kwds组合起来
 //一起传给 __init__
 //其中 obj 会传给 self,
 int res = type->tp_init(obj, args, kwds);
 //......
 return obj;
}
로그인 후 복사

所以源码层面表现出来的,和我们在 Python 层面看到的是一样的。

小结

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

另外,对象调用远不止我们目前说的这么简单,更多的细节隐藏在了幕后,只不过现在没办法将其一次性全部挖掘出来。

위 내용은 소스 코드 탐색: Python에서 객체는 어떻게 호출되나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:51cto.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿