我們知道物件被創建,主要有兩種方式,一種是透過Python/C API,另一種是透過呼叫類型對象。對於內建類型的實例物件而言,這兩種方式都是支援的,例如列表,我們即可以透過[]創建,也可以透過list(),前者是Python/C API,後者是呼叫類型物件。
但對於自訂類別的實例物件而言,我們只能透過呼叫類型物件的方式來建立。而一個物件如果可以被調用,那麼這個物件就是callable,否則就不是callable。
而決定一個物件是不是callable,就取決於其對應的型別物件中是否定義了某個方法。如果從 Python 的角度看的話,這個方法就是 __call__,從解釋器角度看的話,這個方法就是 tp_call。
呼叫int、str、tuple 可以建立一個整數、字串、元組,呼叫自訂的類別也可以建立出對應的實例對象,說明類型物件是可呼叫的,也就是callable。那麼這些類型物件(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、"你好"、3.14 是其對應的實例對象,這些都沒問題。但type是不是類型對象,顯然是的,雖然我們稱呼它為元類,但它也是類型對象,如果 print(type) 顯示的也是一個類別。
那麼相對 type 而言,int、str、float 是不是又變成了實例物件呢?因為它們的類型是 type。
所以class 具有二象性:
同理type 的類型是也是type,那麼type 既是type 的類型對象,type 也是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, "__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; }
因此从上面我们可以看到关键的部分有两个:
所以这对应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返回的不是对应类型的实例对象的指针,比如type_call中第一个参数接收的&PyFloat_Type,但是tp_new中返回的却是PyLongObject *,所以此时就不会执行tp_init。
以上面的代码为例,我们Girl中的__new__应该返回Girl的实例对象才对,但实际上返回了整型,因此类型不一致,所以不会执行__init__。
所以都说 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中文網其他相關文章!