对象模型
Everything Is Object
Python是一个支持多范式的编程语言,你可以使用Python编写面向过程的代码,也可以编写面向对象的代码,甚至函数式编程也不在话下。但是在内部机制上,Python是彻彻底底基于面向对象的思想实现的,整数、列表、字典、函数,甚至代码本身,都以对象的形式存在。
那什么是对象呢?
对象就是对内存数据的抽象。这个抽象出来的东西具有某些特征(属性)、可以进行某些行为(方法)。
为什么函数也是对象呢?如果一个对象可以进行“调用”这个行为,那么你就可以这个对象看成函数。
这也是Python中鸭子类型的体现。所谓鸭子类型,就是“如果一个东西看起来像鸭子,叫起来也像鸭子,那么它就是一只鸭子”,核心思想就是只关心对象的行为或特征,对这个对象到底是什么并不关心。
这种思想可以带来良好的一致性。比如现在的场景是从一个容器中一个一个的取出元素进行操作,那么这个容器是列表还是元组重要吗?不重要。只需可以从这个容器里一个一个取出元素就行了。甚至不一定需要是一个容器。你在迭代range
对象的时候关心过它是什么吗?
In [1]: r = range(3)
In [2]: type(r)
Out[2]: range
In [3]: for i in r:
...: print(i)
...:
0
1
2
[!NOTE]
像函数这种可以调用的对象,我们就称之为,嗯,可调用对象。
由于可调用对象的存在,Python中的属性和方法没有本质的区别。
obj.func()
逻辑上其实就是(obj.func)()
。
内存布局
Python对象对应的内存块是靠指针连结在一起的。例如一个保存了1, 2, 3
三个整数的列表对象,其内存布局如下图。

这个例子很好理解。结构体变量用于保存列表对象的基本信息,然后开辟一个动态数组,动态数组内的元素都是指针,用于指向其它的对象。这样,列表就可以“保存”任意多的任意对象了。
列表对象就是对黄色框线内的两个内存块的抽象。
其它对象的结构也大抵如此。一个核心内存块(我自己取的名字,非官方)保存基本信息,若干其它内存块保存数据。也有些对象不需要保存数据,或者数据直接保存在核心内存块中。
列表对象动态数组的指针指向的内存块,逻辑上不应该属于列表对象。例如图中三个浅蓝色的123内存块,是3个独立的整数对象。
同时由于列表是变长的,为了避免每次长度改变都要重新分配内存,在为动态数组分配内存时通常会多分配一些余量,删除元素时也不是立马缩容,因此动态数组尾部通常都有一些空指针,它们对列表对象的行为没有影响,但会被sizeof计量。
参考下面的示例代码:
In [1]: [1, 2, 3].__sizeof__()
Out[1]: 72
In [2]: [1, 2, 3, 4].__sizeof__()
Out[2]: 72
In [3]: list(range(8)).__sizeof__()
Out[3]: 104
In [4]: sum(x.__sizeof__() for x in range(8))
Out[4]: 224
为什么内存块会有结构体变量和动态数组两种形式呢?
以列表对象举例来说,一个列表需要保存的基本信息是固定的,也就是所需的内存大小以及每个字段的含义都是确定的,所以事先定义一个结构体即可。但是对于“保存”元素的数组,没有办法事先确定这个列表会保存多少个元素,自然也只能动态地进行分配。
[!TIP]
理解了列表的内存布局之后,关于列表和元组的区别就很好理解了。元组和列表的内存布局基本一致,但是动态数组的内容不可以修改,也就是这些指针不能再指向其它对象了,那么从Python层面来看就是元组的内容不可修改。
但是正如前面提到的,这些指针指向的对象是独立的,元组没有办法干涉它们的行为,如果某个指针指向的是一个列表,那么这个列表是可以随意修改的。
总结一下:内存块内容可变的对象是可变对象,内存块内容不可变的对象是不可变对象,但是需要注意这些对象对应的内存块的范围,不属于它的内存块它无法约束其可变性。
名字绑定
当你在Python中创建一个对象之后,内存中就多了若干个已分配的内存块。日后想要使用这个对象,就要先找到这些内存块(只需要找到核心内存块就行了,其它内存块可以通过核心内存块找到)。Python使用两种方式定位这些内存块。
一种是前面提到的指针,另一种也是指针。
第一种应该十分好理解:
In [1]: ls = [1, 2, 3]
In [2]: ls[0] + ls[1]
Out[2]: 3
在使用ls[0]
的时候,Python在背后默默做了这些:找到ls
动态数组中的第0个指针→找到这个指针指向的目标对象→使用目标对象。
第二种的官方名称叫做名字绑定。原理是这样的:

图中两个指针是绑定在一起的,其中一个指针指向一个字符串对象'a'
,另外一个指针指向了一个整数对象1
,那么字符串'a'
和整数1
就绑定在一起了。
这个a
通常还有一个形象但不确切的名字——变量。
而且这种绑定关系会让我们想起一种熟悉的数据结构——字典。没错,大多数时候名字绑定都是用字典实现的:
In [1]: import sys
In [2]: a = 1
In [3]: d = sys.modules['__main__'].__dict__
In [4]: type(d)
Out[4]: dict
In [5]: 'a' in d
Out[5]: True
In [6]: d['a']
Out[6]: 1
上面示例中的字典被称为全局字典,因为这个字典对整个模块都是有效的。
在全局字典之后,还有一个内建字典,print
和list
等就藏身在这里:
In [1]: import builtins
In [2]: 'print' in builtins.__dict__
Out[2]: True
In [3]: print is builtins.__dict__['print']
Out[3]: True
这两个字典就是LEGB规则中的G和B。
[!TIP]
Local、Enclosed、Global、Built-in是Python中的四层命名空间,优先级从高到低。
if __name__ == '__main__'
里面的__name__
,就是Global空间中的一个名字。
常用的名字绑定操作有以下几个:
# 赋值
a = 1
# 模块导入
import xxx # 将xxx和一个模块对象绑定,这个绑定关系记录在全局字典中
from xx import xxx
# as关键字
with xxx as xx
import xxx as xx
except xxx as xx
# 类定义和函数定义
class xx # xx会和一个类对象绑定
def xx # xx会和一个函数对象绑定
# for循环、调用传参
for x in ls # x会依次和列表元素进行绑定
func(x) # 在函数内部,被传入的实参和名字'x'绑定
[!IMPORTANT]
Python这种机制存在一些潜在的问题,不同的名字绑定到同一个对象,或者不同的指针指向同一个对象时,如果这个对象是可变的,那么改变这个对象时,这些名字就会“一荣俱荣,一损俱损”(因为它们本来就是同一个)。
In [1]: a = [1, 2, 3] In [2]: b = a In [3]: ls = [a, b] In [4]: tp = (a, b) In [5]: a[1] = 0x3f In [6]: print(a, b, ls, tp) [1, 63, 3] [1, 63, 3] [[1, 63, 3], [1, 63, 3]] ([1, 63, 3], [1, 63, 3]) In [7]: a is b and b is ls[0] and ls[1] is tp[0] Out[7]: True
[!IMPORTANT]
除了全局字典和内建字典之外,许多Python对象都有一个自己的属性字典用来保存自己的属性,也可以用
.__dict__
获取这个字典。
特殊方法
Python的一大特点是其运算符(+ - * /
等)和内置函数(abs() len()
等)是通过调用对象的特殊方法(也叫魔术方法)实现的,这种特性可以方便的实现运算符重载,比如下面的例子:
In [1]: from __future__ import annotations
...: import math
In [2]: class Vector:
...:
...: def __init__(self, x: int, y: int) -> None:
...: self._x = x
...: self._y = y
...:
...: def __repr__(self) -> str:
...: return f"Vector({self._x}, {self._y})"
...:
...: def __add__(self, other: Vector) -> Vector:
...: if not isinstance(other, Vector):
...: raise TypeError
...: return Vector(self._x + other._x, self._y + other._y)
...:
...: def __abs__(self) -> float:
...: return math.sqrt(self._x * self._x + self._y * self._y)
...:
In [3]: a = Vector(1, 2)
In [4]: b = Vector(3, 4)
In [5]: a + b
Out[5]: Vector(4, 6)
In [6]: print(abs(a), abs(b), abs(a + b), sep='\n')
2.23606797749979
5.0
7.211102550927978
In [7]: a + 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[7], line 1
----> 1 a + 1
Cell In[2], line 12, in Vector.__add__(self, other)
10 def __add__(self, other: Vector) -> Vector:
11 if not isinstance(other, Vector):
---> 12 raise TypeError
13 return Vector(self._x + other._x, self._y + other._y)
TypeError:
在形式上,特殊方法以__
开头并以__
结尾。
Python支持的特殊方法非常多,足够再开一个章节了,这里先简单提一下特殊方法的概念和作用,然后思考另外一个重要的问题:我可能创建无数个Vector对象,每个对象都支持调用.__add__()
方法,那每个对象都要复制一份这个方法吗(方法的本质是函数,函数也是一种对象)?
显然是不需要的。不同Vector对象调用.__add__()
方法时的行为模式都是一致的,只是传入的参数不同,只需要把这个方法保存一份,每个对象都去调用这个方法就好了。
那么存在哪呢?
存在它的类型对象里。
类型对象
我们换一个简单的类做示例:
In [1]: class Dog:
...:
...: def __init__(self, name):
...: self.name = name
...:
...: def hello(self):
...: print(f"Hello, I'm {self.name}")
这段代码执行时不会有任何输出,好像什么都没发生,但实际上Python已经在背后创建了一个类型对象,然后把这个类型对象和名字Dog
绑定在了一起。
In [2]: id(Dog)
Out[2]: 1879669847104
In [3]: import sys
In [4]: 'Dog' in sys.modules['__main__'].__dict__
Out[4]: True
In [5]: id(sys.modules['__main__'].__dict__['Dog'])
Out[5]: 1879669847104
上面这段代码证明了两件事:确实有一个对象真实存在于内存当中,它的地址是1879669847104
;这个对象和名字Dog
绑定在了一起,绑定关系记录在一个字典中。
[!TIP]
Python标准中规定,每一个Python对象都有一个标识符(identifier),用来唯一的标识一个对象。内置函数
id()
就是返回一个对象的唯一标识符。语言标准没有规定这个标识符到底是什么,但是在具体实现中,一般使用对象的内存地址作为标识符,因为这是天然唯一的。
也可以用以下代码佐证构造类型对象的代码确实是被执行了:
In [1]: class Dog:
...:
...: print('Start building Dog-type')
...:
...: def __init__(self, name):
...: self.name = name
...:
...: def hello(self):
...: print(f"Hello, I'm {self.name}")
...:
...: print('Dog-type has been built')
...:
Start building Dog-type
Dog-type has been built
也可以更直接一点,把这段代码编译成字节码:
In [1]: from dis import dis
In [2]: dis("""\
...: class Dog:
...: def __init__(self, name):
...: self.name = name
...:
...: def hello(self):
...: print(f"Hello, I'm {self.name}")
...: """)
0 RESUME 0
1 LOAD_BUILD_CLASS
PUSH_NULL
LOAD_CONST 0 (<code object Dog at 0x000001850BD357A0, file "<dis>", line 1>)
MAKE_FUNCTION
LOAD_CONST 1 ('Dog')
CALL 2
STORE_NAME 0 (Dog)
RETURN_CONST 2 (None)
这里只截取了第一段字节码,这些字节码的含义大概如下:
-
RESUME 0
:用于程序控制,可以忽略。 -
LOAD_BUILD_CLASS
:把类型构造函数(__build_class__
)加载到栈中。 -
PUSH_NULL
:与RESUME
配合使用。 -
LOAD_CONST 0
:加载第0个常量,这个例子中这个常量是一个代码对象。 -
MAKE_FUNCTION
、LOAD_CONST
、CALL
:构造一个类型对象的具体操作。 -
STORE_NAME 0
:把这个类型对象和名字0(这个例子中是Dog
)绑定。 -
RETURN_CONST 2
:将第2个常量(这个例子中是None
)作为返回值返回。
在明确有一个实实在在的类型对象之后,就可以着手研究它和实例对象(实例对象就是用某个类型对象实例化出来的对象,例如下面的peter
)的关系了:
In [1]: class Dog:
...:
...: def __init__(self, name):
...: self.name = name
...:
...: def hello(self):
...: print(f"Hello, I'm {self.name}")
In [2]: peter = Dog('peter')
In [3]: type(peter)
Out[3]: __main__.Dog
In [4]: type(peter) is Dog
Out[4]: True
In [5]: peter.__class__ is Dog # 绝大部分情况下,peter.__class__和type(peter)是一致的,都是获取peter的类型对象
Out[5]: True
嗯,它们的关系就是:Dog
是peter
的类型,peter
是Dog
的一个实例。
[!IMPORTANT]
我们可以这么理解类型对象和实例对象:类型对象是一种特殊的对象,这种对象可以用来创造新的实例对象,这些实例对象可以有自己的属性,但是它们的行为模式(方法)是固定的。在逻辑上,类型对象类似“人”这个抽象概念,实例对象类似“一个具体的人”。但是计算机中不存在“概念”,只有实实在在的内存数据,所以类型对象也是实实在在存在于内存当中的。
下面是“对象的方法保存在它的类型对象中”的证据:
In [6]: 'hello' in Dog.__dict__
Out[6]: True
In [7]: hello_method = Dog.__dict__['hello']
In [8]: type(hello_method)
Out[8]: function
In [9]: hello_method is Dog.hello
Out[9]: True
In [10]: ? hello_method
Signature: hello_method(self)
Docstring: <no docstring>
File:
Type: function
可以看到.hello
本质上就是一个函数,保存在Dog
这个内存对象中。我们可以直接调用这个函数,达到和peter.hello()
同样的效果:
In [11]: Dog.hello(peter)
Hello, I'm peter
In [12]: hello_method(peter)
Hello, I'm peter
In [13]: peter.hello()
Hello, I'm peter
这个方法(函数)是需要接收一个参数的,参数需要是一个实例对象。此举不难理解,因为方法保存在类型对象中,必须要明确是哪个实例对象调用的这个方法,从而针对不同的实例对象输出不同的结果。
那为什么peter.hello()
就可以直接调用而不用传递self
参数呢?这是因为Python将这个方法和具体实例进行了绑定,生成了一个新的bound method,隐式地帮你传递了self
参数。
In [14]: peter.hello
Out[14]: <bound method Dog.hello of <__main__.Dog object at 0x000001852C1C0D70>>
In [15]: peter.hello is hello_method
Out[15]: False
peter.hello
和Dog.hello
不是同一个东西,所以才会表现的一个不需要传参一个需要传参。
当然了,peter.hello
是Dog.hello
经过处理得到的,所以“实例对象的方法保存在它的类型对象中”这个说法是成立的。
由于类型对象需要保存的信息大体上是固定的——定义对象行为的函数,所以Python在C语言层面定义了一个结构体PyTypeObject
用来作为所有类型对象的核心内存块。这个结构体包含许多指针,多数是函数指针或函数数组指针:
struct _typeobject {
PyTypeObject *ob_type;
const char *tp_name; /* For printing, in format "<module>.<name>" */
/* Method suites for standard classes */
PyNumberMethods *tp_as_number; /* 与数值相关的函数族,例如加减乘除的实现函数。*/
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
/* More standard operations (here for binary compatibility) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;
}
typedef struct _typeobject PyTypeObject;
上面只列出了一部分字段,字段名都清楚地表现出了各自的作用,例如tp_hash
肯定和hash
函数有关,tp_call
肯定和可调用对象有关。
但是ob_type
这个字段明显令人不安,不管是字段名还是字段类型,都明示它指向的是这个对象的类型对象。但是这已经是类型对象的结构体了,所以类型对象也是有类型对象的?
是的。这种类型对象的类型对象叫做元类。
In [16]: Dog.__class__ # 背后就是ob_type
Out[16]: type
In [17]: Dog.__class__ is type
Out[17]: True
Dog
的类型对象就是如假包换的type
。事实上绝大多数类型对象的类型对象都是type
,也就是type
控制了绝大多数类型对象的行为。甚至type
自己的类型对象也是type
——我命由我不由天,自己控制自己的行为,很正常(实现上直接让自己的ob_type
指针指向自己就行了)。
In [18]: type.__class__ is type
Out[18]: True
接下来需要探索的就是peter.hello
和Dog.hello
不同的原因,也是Python的核心机制之一——属性查找。
[!TIP]
除了点号运算符之外,
getattr(<instance>, <attr_name>)
也可以获取对象的属性,两者区别不大。
属性查找
在使用<instance>.<attr>
获取属性时,Python执行的底层C函数是PyObject_GetAttr
,这个函数的核心逻辑如下:
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
PyTypeObject *tp = Py_TYPE(v);
PyObject* result = NULL;
if (tp->tp_getattro != NULL) {
result = (*tp->tp_getattro)(v, name);
} else {}
if (result == NULL) {
_PyObject_SetAttributeErrorContext(v, name);
}
return result;
}
这个函数做的事很简单,首先获取<instance>
的类型对象,然后调用类型对象tp_getattro
指针指向的函数,属性查找的流程都由这个函数控制。
也就是说,如果是peter.hello
,执行的是Dog
的tp_getattro
指向的函数;如果是Dog.hello
,执行的是type
的tp_getattro
指向的函数。
在大多数情况下,类型对象的tp_getattro
会指向一个默认的属性查找函数PyObject_GenericGetAttr
,type
的tp_getattro
则指向另外一个属性查找函数_Py_type_getattro
。这两个函数大体流程相似,但是也存在一些细节差异。
简单来说,PyObject_GenericGetAttr
的查找流程是:
- 类型对象MRO链中的数据描述符。
- 实例对象的属性字典(不触发描述符机制)。
- 类型对象MRO链中的非数据描述符。
AttributeError
。
Python MRO(Method Resolution Order)的结果是一个元组,元组每个元素都是类型对象的父类(第一个元素是类型对象自己)。这个元组的顺序决定了属性查找顺序,也就是类型对象继承其父类属性时的优先级——先被查找的父类自然拥有更高的优先级。
以常见的Enum为例,它的MRO是这样的(所有的类都隐式地继承object
):
In [1]: from enum import Enum
In [2]: class ColorEnum(str, Enum):
...: RED = '#f00'
...: GREEN = '#0f0'
...: BLUE = '#00f'
...:
In [3]: ColorEnum.__mro__
Out[3]: (<enum 'ColorEnum'>, str, <enum 'Enum'>, object)
[!TIP]
Python的MRO是通过C3算法确定的。对于同一级的父类,写在前面的父类优先级更高。对于不同级的父类,优先级类似于DFS,但是公共祖先会被放到最后。
“类型对象MRO链中的数据描述符”,意思就是按照MRO的顺序,依次在这些类的属性字典__dict__
中查找是否有这个属性名,如果属性名对应的value是一个数据描述符,那么就调用这个数据描述符的__get__
方法并将结果返回。
所谓数据描述符,就是定义了__get__
方法,且至少定义了__set__
/__delete__
二者之一的对象。非数据描述符就是只定义了__get__
方法的对象。
In [1]: class DataDescriptor:
...:
...: def __get__(self, instance, owner):
...: return "test"
...:
...: def __set__(self, instance, value):
...: pass
...:
...: def __delete__(self, instance):
...: pass
...:
In [2]: class NonDataDescriptor:
...:
...: def __get__(self, instance, owner):
...: return "test"
...:
In [3]: class A:
...: attr = DataDescriptor()
...:
...: def __init__(self):
...: self.attr = "A"
...:
In [4]: class B:
...: attr = NonDataDescriptor()
...:
...: def __init__(self):
...: self.attr = "B"
...:
In [5]: a, b = A(), B()
In [6]: a.attr, b.attr
Out[6]: ('test', 'B')
在这个例子中:
a.attr
在A
的MRO中发现了一个名为attr
的数据描述符,所以直接调用这个数据描述符的__get__
方法返回字符串"test"
。
b.attr
在B
的MRO中没有发现名为attr
的数据描述符(只有一个非数据描述符),所以继续在b
的__dict__
中查找,发现了名为attr
的属性,直接返回。
而且由于A
中的attr
同时定义了__set__
和__delete__
,属性赋值和属性删除也会受到影响:
In [7]: a.attr = 'A'
In [8]: a.attr
Out[8]: 'test'
In [9]: del a.attr
In [10]: a.attr
Out[10]: 'test'
_Py_type_getattro
的查找流程稍有不同:
- 元类MRO链中的数据描述符。
- 类型对象MRO链中的属性(遵循描述符机制)。
- 元类MRO链中的非数据描述符。
AttributeError
。
主要区别就是始终遵循描述符机制,即使是在自己的__dict__
中。
到这里还无法解释peter.hello
和Dog.hello
的区别,因为这两个都是从Dog
的__dict__
中找到hello
然后执行hello.__get__
得到的。所以玄机就藏在__get__
的执行过程中。
所有描述符的__get__
都接收三个参数:
self
:描述符自身。instance
:实例对象。owner
:描述符所在的对象。
在peter.hello
中,instance
是peter
,owner
是Dog
。在Dog.hello
中,由于不是从实例对象开始的属性查找,这里的instance
是None
。而hello
的__get__
方法就根据instance
的不同返回了不同的结果。我们可以模拟一下这个过程:
In [1]: class Descriptor:
...:
...: def __get__(self, instance, owner):
...: if instance is None:
...: return "class call"
...: else:
...: return "instance call"
...:
In [2]: class A:
...: attr = Descriptor()
...:
In [3]: A.attr
Out[3]: 'class call'
In [4]: A().attr
Out[4]: 'instance call'
在实际的hello
中,当instance
是None
的时候,__get__
返回hello
本身;当instance
是Dog
的实例对象时,__get__
返回一个bound method,将instance
自动传递到hello
的self
上,避免了手动传参。
属性查找到这里就告一段落了,接下来去探索一下前面提到的元类。
[!TIP]
除了以上两个属性查找函数之外,Python还准备了两个函数用于处理定义了
__getattribute__
或__getattr__
的情况,以下是源码中的注释。/* There are two slot dispatch functions for tp_getattro. - _Py_slot_tp_getattro() is used when __getattribute__ is overridden but no __getattr__ hook is present; - _Py_slot_tp_getattr_hook() is used when a __getattr__ hook is present. The code in update_one_slot() always installs _Py_slot_tp_getattr_hook(); this detects the absence of __getattr__ and then installs the simpler slot if necessary. */
但是如无必要,不要轻易定义这些影响核心机制的方法。
元类
所谓元类,就是可以创造出其它类的类。元类可以控制由它创造的类的行为。所以当你需要控制一个类的行为的时候,你就可以定义一个元类。
[!NOTE]
注意区分实例的行为和类的行为。
一个类最常见的行为就是创建一个实例。
通常情况下,这个行为由type
控制,但是你可以自定义一个元类,控制实例创建行为,从而实现单例模式:
In [1]: class SingletonMeta(type):
...: _instances = {}
...:
...: def __call__(cls, *args, **kwargs):
...: if cls not in cls._instances:
...: cls._instances[cls] = super().__call__(*args, **kwargs)
...: return cls._instances[cls]
...:
In [2]: class Singleton(metaclass=SingletonMeta):
...: def __init__(self, value):
...: self.value = value
...:
In [3]: obj_1 = Singleton(1)
In [4]: obj_2 = Singleton(2)
In [5]: print(obj_1.value, obj_2.value)
1 1
In [6]: obj_1 is obj_2
Out[6]: True
原理很好理解:Python在调用类型对象创建一个实例对象时,会直接调用它的元类的__call__
方法。在Singleton
的元类SingletonMeta
的__call__
方法中,如果Singleton
有对应的实例,就直接返回这个实例,所以Singleton(2)
得到的其实是Singleton(1)
的结果,也就是实现了单例对象的效果。
从SingletonMeta
的__call__
的逻辑可以看出,type
(super()
的结果就是type
)的__call__
才是真正地对象构造方法。
对象构造
以Dog('peter')
为例,type
的__call__
方法有两个主要步骤:
- 调用
Dog
的__new__
方法。如果没有自定义,会默认继承object
的兜底__new__
方法。这个兜底方法的主要作用是为实例对象分配内存和一些必要的初始化工作(例如填充__class__
指针等),然后返回分配的核心内存块的地址(指针)。 - 调用
Dog
的__init__
方法。这个方法接受上一步返回的指针(也就是Python层面的self
)和一些额外参数,然后进行主要的初始化工作。
也就是说Python的对象构造分为内存分配和属性初始化两个阶段。
[!TIP]
虽然大多数时候调用一个类型对象的目的就是创建一个实例化对象,但是也有一个例外——
type
。调用type
的目的是获取一个对象的类型对象。由于type
是自己的元类,所以type()
也由type
的__call__
来控制,因此在__call__
里其实还进行了特判:如果传进来的cls
是type
自己,就按照获取类型对象的流程走。