ホームページ  >  記事  >  バックエンド開発  >  Python オブジェクトの呼び出し方法

Python オブジェクトの呼び出し方法

WBOY
WBOY転載
2023-05-31 11:39:111056ブラウズ

## wedge


オブジェクトを作成するには主に 2 つの方法があることがわかりました。1 つは Python/C API を使用する方法、もう 1 つは Python/C API を使用する方法です。これは、型オブジェクトを呼び出すことによって行われます。組み込み型のインスタンス オブジェクトについては、両方のメソッドがサポートされています。たとえば、リストは [] または list() を通じて作成できます。前者は Python/C API で、後者は呼び出し型オブジェクトです。


カスタム クラスのインスタンス オブジェクトの作成は、クラスの型オブジェクトを呼び出すことによってのみ実行できます。オブジェクトは、呼び出すことができる場合は呼び出し可能ですが、それ以外の場合は呼び出すことができません。


オブジェクトが呼び出し可能かどうかは、対応する型オブジェクトが特定のメソッドを定義しているかどうかによって決まります。 Python の観点から見ると、このメソッドは __call__ であり、インタプリタの観点から見ると、このメソッドは tp_call です。


Python の観点からオブジェクト呼び出しを見てみる


int、str、または tuple を呼び出すことで、整数、文字列、またはタプルを作成できます。また、次のように呼び出すこともできます。カスタム クラス 対応するインスタンス オブジェクトを作成し、型オブジェクトが呼び出し可能であること、つまり呼び出し可能であることを示します。次に、これらの型オブジェクト (int、str、タプル、クラスなど) の型オブジェクト (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 はそれらに対応するものであると言います。インスタンスオブジェクト、これは問題ありません。しかし、type は型オブジェクトですか? もちろん、メタクラスと呼んでいますが、型オブジェクトでもあります。print(type) でクラスを表示する場合、それも型オブジェクトです。


ということは、type に関連して、int、str、float が再びインスタンス オブジェクトになるのでしょうか?彼らのタイプはタイプだからです。


したがって、クラスには二重性があります:


  • インスタンス オブジェクト (例: 123、"Satori"、[]、3.14) の上に立った場合) 型の観点から見ると、それは型オブジェクトです

  • #型の観点から見ると、それはインスタンス オブジェクトです


  • # #type と同様に、of の type も type である場合、type は type の type オブジェクトであり、type は type のインスタンス オブジェクトでもあります。やや複雑な説明ですが、理解するのは難しくないはずです。以降の説明でのあいまいさを避けるために、ここで次のように記述します:


    整数、浮動小数点数、文字列などをインスタンス オブジェクトと呼びます

  • int、float、str、dict、およびカスタム クラスを型オブジェクトと呼びます

  • type これも型オブジェクトですが、 、しかし、私たちはそれをメタクラスと呼びます

  • つまり、型の中に __call__ メソッドがあります。これは、型オブジェクトを呼び出すことは type __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 を使用してプロパティを動的に追加、削除、または変更することはできません。ソース コードから、メタクラスを含むこれらの組み込みクラスはすべて PyTypeObject オブジェクトであり、最下位レベルでグローバル変数として宣言されているか、静的クラスとしてすでに存在していることがわかります。 type はすべてのタイプのオブジェクトのメタクラスですが、追加、削除、変更できるのはカスタム クラスを扱う場合のみです。


また、Python の動的な性質は、インタープリターがバイトコードを C コードに変換するときに動的に割り当てられることも説明しました。したがって、クラスの属性またはメソッドを動的に設定することは、動的クラスにのみ適用されます。 、py ファイルの class キーワードを使用して定義されたクラス。


拡張モジュールの作成時に定義された静的クラスまたは拡張クラス (2 つは同等) については、コンパイル後にすでに 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'

組み込み/拡張型辞書の属性を設定できないことを示す例外がスローされました。これは、組み込み/拡張型辞書はインタープリタの解釈と実行ステップをバイパスするため、属性を動的に設定できないためです。


同じことがインスタンス オブジェクトにも当てはまります。静的クラスのインスタンス オブジェクトは属性を動的に設定できません:

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__。一般来说,除了cls,__new__方法的参数通常包括*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:type(instance):"""

这个句子可以重写为:“有很多需要讨论的问题,最先引起注意的是在 __init__ 中需要传入两个参数,然而我们未传入参数时并未报错。”。原因就在于这个 __init__ 压根就没有执行,因为 __new__ 返回的不是 Girl 的实例对象。

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

class Girl:
 def __new__(cls, *args, **kwargs):
 return 123
 def __init__(self, name, age):
 print("__init__方法执行啦")
g = Girl()
print(g, type(g))# 123

我们看到打印的是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 オブジェクトの呼び出し方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。