KBEngine 内嵌 Python 3.7.3 数据模型详解:对象、类型层级与特殊方法机制
KBEngine 内嵌 Python 3.7.3 数据模型详解:对象、类型层级与特殊方法机制
在 KBEngine 这款服务端 MMOG 引擎中,游戏逻辑(实体属性、远程方法、定时器、登录与空间逻辑等)是以 Python 脚本承载并运行在引擎内嵌的 CPython 解释器之上的。因此,要读懂、写出可靠的 KBEngine 服务端脚本,必须先吃透 CPython 的「数据模型(Data model)」——它规定了对象如何表示数据、类型如何约束行为、以及语言运算符如何被「运算符重载」映射为类上的特殊方法(dunder 方法)。本篇以 KBEngine 源码树中内嵌的 datamodel.rst 这一官方参考章节为核心骨架,系统讲解对象三要素、标准类型层级、特殊方法名体系、描述符与元类机制,并结合 KBEngine 的 C++/Python 桥接层(pyscript)说明这些数据模型概念在引擎中如何被真正使用。
读完后你应当能够:独立解释一个 Python 对象的 identity / type / value 与可变性语义;熟练列出并运用各分类的特殊方法名(含上下文、容器、数值、属性访问、描述符);理解描述符与 __slots__ 的查找优先级;并能据此判断 KBEngine 中那些由引擎暴露给脚本的容器对象为何表现得像 dict/list。
一、对象、值与类型
Python 用「对象」抽象一切数据:程序中的所有数据都由对象、或对象之间的关系来表示。从冯·诺依曼「存储程序」计算机模型的角度看,代码本身也由对象表示。
每个对象都有三个维度:identity(身份)、type(类型)、value(值)。
- 身份:一旦创建就永不改变,可理解为对象在内存中的地址。
is运算符比较身份;id()返回一个代表身份(在 CPython 中即存储地址)的整数。 - 类型:决定对象支持哪些操作(例如「是否有长度」),并定义该类型对象的可能取值。
type()返回对象的类型(类型本身也是对象)。与身份一样,类型也不可改变(个别受控条件下可改变,但通常不推荐)。 - 值:部分对象的值可改变。值可改变者称为 mutable(可变),创建后不可变者称为 immutable(不可变)。例如数字、字符串、元组不可变,字典、列表可变。
这里有个微妙点:一个不可变容器若持有一个可变对象的引用,当那个可变对象被修改时,容器的「值」也会变化,但容器本身仍被归为不可变——因为「它所直接包含的对象集合」没有变。所以「不可变」并不严格等同于「值永远不变」。
垃圾回收方面:对象从不被显式销毁;当它变得不可达(unreachable)时,可能被垃圾回收。CPython 采用「引用计数 + (可选的)循环垃圾延迟检测」,会在对象不可达时尽快回收大部分对象,但不保证能回收含循环引用的垃圾。文档特别提醒:不要依赖对象在不可达后立即被终结化,因此应当显式关闭文件等外部资源。此外,实现层的 tracing/debug 设施、以及 try...except 捕获到的异常都可能让本应可回收的对象继续存活。
容器(container):持有对其它对象引用的对象(如 tuple、list、dict)称为容器,这些引用是容器值的一部分。讨论容器「值」时通常指所含对象的值而非身份;而讨论容器「可变性」时只涉及其直接包含对象的身份。
类型几乎影响对象行为的所有方面。对不可变类型,计算新值的操作实际上可以返回任一「同类型同值」的已存在对象(例如 a=1; b=1 后 a、b 可能指向同一对象);而对可变对象则不允许这种复用(c=[]; d=[] 一定得到两个不同的新空列表)。注意 c = d = [] 则是把同一对象赋给两个名字。
二、标准类型层级
以下是 Python 内建类型清单。扩展模块(用 C、Java 等语言编写,取决于实现)可定义额外类型;未来版本可能向类型层级新增类型,但通常通过标准库提供。类型说明中列出的「特殊属性」是用于访问实现细节、不供通用使用的属性,其定义未来可能改变。
单值类型:
None:只有一个值与对象,通过内建名None访问,表示「缺少值」(未显式return的函数即返回它),真值为假。NotImplemented:单值对象,数值方法与富比较方法在「不支持给定操作数」时应返回它,解释器会尝试反射操作或其它回退,真值为真。Ellipsis:单值对象,通过字面量...或内建名Ellipsis访问,真值为真。
数字(numbers.Number):由数字字面量创建、由算术运算符与内建函数返回;不可变。分为:
numbers.Integral:整数(int,范围仅受虚拟内存限制,负数采用「无限符号位」的二补码变体)与布尔(bool,False/True两个唯一对象,是int的子类型,数值上表现为 0/1,仅转字符串时分别得到"False"/"True")。numbers.Real(float):机器级双精度浮点,范围与溢出取决于底层架构(C/Java 实现);Python 不提供单精度浮点。numbers.Complex(complex):由一对机器级双精度浮点构成的复数,实部虚部通过只读属性z.real、z.imag读取。
序列(Sequences):用非负整数索引的有限有序集合;len 返回元素数。支持切片 a[i:j](选出满足 i <= k < j 的项),扩展切片 a[i:j:k] 选出 x = i + n*k 的项。按可变性分两类:
- 不可变序列:
str(Unicode 码点序列,ord/chr相互转换,str.encode/bytes.decode互转)、tuple(单元素元组需加逗号,空元组为一对空括号)、bytes(b'abc'或bytes()构造的不可变 8 位数组)。 - 可变序列:
list、bytearray(可变数组,因可变而不可哈希);扩展模块array、collections也提供可变序列类型。
集合类型(Set types):由唯一、不可变对象组成的无序有限集,不能用下标索引,但可迭代、len 返回大小;常用于快速成员判定、去重、求交/并/差/对称差。元素适用与字典键相同的不可变规则;1 与 1.0 比较相等,故只能共存其一。两种内建:set(可变,set() 构造,add 等修改)与 frozenset(不可变、可哈希,可再作为集合元素或字典键)。
映射(Mappings):用任意索引集索引的有限对象集,a[k] 取出键 k 对应的项,len 返回项数。唯一内建映射是 dict:键不能是含列表/字典等「按值比较的可变类型」,因为字典的高效实现要求键的哈希值恒定;数值键遵循数值比较规则(1 与 1.0 可互换索引同一项)。扩展模块 dbm.ndbm/dbm.gnu、collections 也提供映射类型。
可调用类型(Callable types):能对其施加「函数调用操作」的类型,包括:用户自定义函数、实例方法、生成器函数(用 yield,调用后返回迭代器)、协程函数(async def,返回协程对象)、异步生成器函数(async def + yield)、内建函数(C 函数封装)、内建方法(如 alist.append())、类(作为工厂)、类实例(定义 __call__ 后可被调用)、模块。
其中「用户定义函数」有一组可写/只读特殊属性,如表所示:
| 属性 | 含义 | 可写性 |
|---|---|---|
__doc__ |
文档字符串,或 None;不被子类继承 |
可写 |
__name__ |
函数名 | 可写 |
__qualname__ |
限定名(3.3 新增) | 可写 |
__module__ |
定义所在模块名,或 None |
可写 |
__defaults__ |
含默认值参数的默认值元组,或 None |
可写 |
__code__ |
表示编译后函数体的代码对象 | 可写 |
__globals__ |
函数全局变量字典(定义模块的全局命名空间) | 只读 |
__dict__ |
支持任意函数属性的命名空间 | 可写 |
__closure__ |
None 或包含自由变量绑定的 cell 元组 |
只读 |
__annotations__ |
参数注释字典(含 'return' 键) |
可写 |
__kwdefaults__ |
仅关键字参数的默认值字典 | 可写 |
大多数标为「可写」的属性会校验赋值值的类型。函数对象还可通过普通点号存取任意属性(如附加元数据)。cell 对象有 cell_contents 属性用于读写其值。
实例方法结合类、类实例与一个可调用对象(通常是用户函数)。只读属性:__self__ 是类实例对象,__func__ 是函数对象,__doc__/__name__/__module__ 分别对应 __func__ 的同名属性。当通过某实例从类中取到一个用户定义函数时,会创建一个「绑定」方法对象(__self__ 为实例,__func__ 为原函数);调用 x.f(1) 等价于 C.f(x, 1)。由类方法派生的实例方法,其 __self__ 实为类本身,调用 x.f(1) 或 C.f(1) 都等价于 f(C, 1)。注意:函数到实例方法的转换在「每次从实例取属性」时发生,因此局部变量缓存方法再调用是一种有效优化;且该转换只对「类的属性」生效,不对「实例的属性」生效。
**模块(Modules)**是 Python 代码的基本组织单元,由 import 系统创建。模块对象有一个用字典实现的命名空间(即该模块内函数 __globals__ 所引用的字典):m.x 等价于 m.__dict__["x"],m.x = 1 等价于 m.__dict__["x"] = 1。预定义可写属性:__name__(模块名)、__doc__(文档字符串或 None)、__annotations__(可选,模块体执行期间收集的变量注释)、__file__(模块加载自文件的完整路径,对静态链入的 C 模块可能缺失,对动态加载的扩展模块则为共享库文件路径)。只读特殊属性 __dict__ 即模块命名空间字典。实现细节:CPython 清空模块字典的方式可能导致「即使字典仍有活引用,模块字典也会在模块离开作用域时被清空」,因此直接引用其字典时应先拷贝或保留模块。
自定义类由类定义创建,命名空间也是字典;C.x 翻译为 C.__dict__["x"](存在其它查找钩子)。若未找到,则在基类中按 C3 方法解析序(MRO)继续查找,即使在「菱形」继承下也能正确工作。类属性赋值只会更新该类的字典,绝不更新基类的字典。类的特殊属性:__name__、__module__、__dict__(类命名空间字典)、__bases__(基类元组,按定义顺序)、__doc__、__annotations__(可选)。
类实例通过调用类对象创建,其命名空间是一个字典,是属性查找的第一站;未找到且类中有同名属性时继续查类属性,若命中用户定义函数则转换为 __self__ 为该实例的实例方法(静态方法、类方法亦会转换);都找不到且类定义了 __getattr__ 时才调用它以满足查找。属性赋值与删除只更新实例字典(除非类定义了 __setattr__/__delattr__)。特殊属性 __dict__(属性字典)与 __class__(实例的类)。
**I/O 对象(文件对象)**代表一个打开的文件,可由 open、os.popen、os.fdopen、socket.makefile 等创建。sys.stdin/stdout/stderr 初始化为对应解释器标准流、以文本模式打开的文件对象,遵循 io.TextIOBase 接口。
**内部类型(Internal types)**是被暴露给用户的解释器内部类型,定义可能随版本变化:
- 代码对象:表示字节编译后的 Python 代码(字节码)。与函数对象的差别在于:函数对象显式引用全局命名空间,而代码对象不含上下文;默认参数值存在函数对象而非代码对象中(因为它们是运行期算出的值)。代码对象不可变、不引用可变对象。只读属性包括
co_name、co_argcount、co_nlocals、co_varnames、co_cellvars、co_freevars、co_code(字节码字符串)、co_consts(字面量元组)、co_names、co_filename、co_firstlineno、co_lnotab、co_stacksize、co_flags。co_flags位:0x04(*args)、0x08(**kwargs)、0x20(生成器);0x2000(future division);0x10/0x1000用于更早版本。若代码对象代表函数,co_consts首项即其文档字符串(无则None)。 - 帧对象:表示执行帧,可出现在 traceback 中、也传给注册的 trace 函数。只读属性
f_back、f_code、f_locals、f_globals、f_builtins、f_lasti;可写属性f_trace(非None时为代码执行各事件调用的函数,调试器用它)、f_trace_lines、f_trace_opcodes、f_lineno(调试器可写它实现「跳转」)。提供frame.clear()方法(3.4 新增),清空局部变量引用并可终结所属生成器,以打破「帧—异常—traceback—局部变量」这类引用环。 - Traceback 对象:表示异常的调用栈,异常发生时隐式创建,也可显式调用
types.TracebackType创建。隐式创建时,栈回溯每展开一层就在当前 traceback 前插入一个新对象;在异常处理器可用时,栈迹作为sys.exc_info()三元组第三项、以及被捕获异常的__traceback__属性暴露。只读属性tb_frame、tb_lineno、tb_lasti;可写属性tb_next(3.7 起可显式实例化并更新tb_next)。 - Slice 对象:表示
__getitem__方法的切片,也可由内建slice创建。只读属性start/stop/step(省略则为None,可为任意类型)。提供slice.indices(length),对给定长度计算start/stop/step三元组。 - Static method 对象:用
staticmethod构造,包装一个对象(通常是用户方法对象),从类/实例取出时返回被包装对象本身、不再转换;它自身不可调用(但其包装物通常可)。 - Class method 对象:用
classmethod构造,行为如上文「用户定义方法」所述。
三、特殊方法名:Python 的运算符重载
类可以通过定义特殊名称的方法来实现由特殊语法触发(如算术运算、下标与切片)的操作。这是 Python 的「运算符重载」方式,让类能自定义其对语言运算符的行为。例如,若类定义了 __getitem__ 且 x 是其实例,则 x[i] 大致等价于 type(x).__getitem__(x, i)。除特别注明外,未定义合适方法时执行该操作会抛异常(通常是 AttributeError 或 TypeError)。把某个特殊方法设为 None 表示对应操作不可用——例如将 __iter__ 设为 None,类就不可迭代,iter 会抛 TypeError(且不再回退到 __getitem__)。
实现「模拟内建类型」的类时,只应在对「被模拟对象」有意义的程度上实现。下面按类别梳理特殊方法名。
基本定制
object.__new__(cls[, ...]):静态方法(特判,无需声明),用于创建 cls 的新实例;返回新实例(通常是 cls 的实例)。典型实现是调用super().__new__(cls)创建后修改再返回。若返回 cls 的实例,则随后按__init__(self[, ...])调用其__init__;否则不调用。__new__主要供「不可变类型(int/str/tuple 等)的子类」定制实例创建,也常在自定义元类中重写以定制类创建。object.__init__(self[, ...]):在实例由__new__创建之后、返回给调用者之前被调用。若基类有__init__,派生类应显式super().__init__([args...])以确保基类部分初始化。__new__与__init__协同(__new__创建、__init__定制),故__init__不得返回非None值,否则运行期抛TypeError。object.__del__(self):实例即将销毁时调用(finalizer/destructor)。若基类有__del__,派生类应显式调用。del x并不直接调用x.__del__()——前者只是把引用计数减一,后者只在引用计数归零时被调用。__del__在解释器退出时未必对仍存在的对象调用;且在它被调用的「危急时刻」抛出的异常会被忽略、只向sys.stderr打印警告;它可能在任意代码/线程执行期间被触发(可能死锁),也可能在全局变量已被删除/置None的关闭阶段运行(以单下划线开头的名字会被先删除,这有助于保证被导入模块仍可用)。object.__repr__(self):由repr计算「官方」字符串表示,理想情况下应看起来像能重建同值对象的合法 Python 表达式,否则返回<...有用描述...>;必须返回字符串。若只定义__repr__未定义__str__,则非正式字符串表示也用它。常用于调试,故应信息丰富且无歧义。object.__str__(self):由str/format/print计算「非正式」可打印表示,必须返回字符串。与__repr__的区别在于不要求返回合法 Python 表达式,object的默认实现调用__repr__。object.__bytes__(self):由bytes计算字节串表示,返回bytes。object.__format__(self, format_spec):由format、f-string、str.format触发;format_spec描述所需格式化选项,解释权交给实现类型;须返回字符串。3.4 起object的__format__对非空字符串抛TypeError;3.7 起object.__format__(x, '')等价于str(x)。- 富比较:
__lt__/__le__/__eq__/__ne__/__gt__/__ge__分别对应</<=/==/!=/>/>=。可返回NotImplemented(表示未对给定操作数实现该操作)。默认__ne__委托__eq__并取反(除非为NotImplemented);比较运算符之间无其它隐含关系((x<y or x==y)的真并不蕴含x<=y),可用functools.total_ordering从单一基础操作自动派生排序。这些方法没有「换参版本」:__lt__与__gt__互为反射、__le__与 ``__ge__互为反射、__eq__/__ne__各自即反射。若操作数类型不同、且右操作数的类型是左操作数类型的直接/间接子类,则右操作数的反射方法优先,否则左操作数方法优先(不考虑虚拟子类型)。 object.__hash__(self):由hash及set/frozenset/dict成员操作调用,应返回整数。唯一硬性要求是「比较相等的对象有相同哈希值」;建议把参与比较的成员打包成元组后哈希,例如return hash((self.name, self.nick, self.color))。注意hash会把自定义__hash__返回值截断到Py_ssize_t(64 位构建通常 8 字节、32 位构建 4 字节),跨位宽构建可用python -c "import sys; print(sys.hash_info.width)"校验宽度。类若未定义__eq__则不应定义__hash__;定义了__eq__却没定义__hash__,其实例不可用于可哈希集合;定义了可变对象并实现了__eq__的类不应实现__hash__(哈希集合要求键哈希不可变)。用户类默认有__eq__/__hash__:默认所有对象互不相等(除与自身),且x==y蕴含x is y与hash(x)==hash(y)。覆盖__eq__却不定义__hash__的类会被隐式置__hash__ = None(尝试取哈希抛TypeError、isinstance(obj, collections.abc.Hashable)判为不可哈希);要保留父类__hash__需显式__hash__ = <ParentClass>.__hash__;不覆盖__eq__又想抑制哈希则写__hash__ = None。str/bytes/datetime 的默认哈希以不可预测随机值「加盐」,进程内恒定、跨进程不可预测,意在抵御利用 dict 插入最坏 O(n²) 的拒绝服务攻击(会改变集合迭代顺序,Python 从未保证该顺序)。object.__bool__(self):实现真值测试与bool(),应返回False/True。未定义时若定义了__len__则用其结果是否非零判定;两者都未定义则实例恒为真。
定制属性访问
以下方法可定制类实例的属性访问(x.name 的读取、赋值、删除):
object.__getattr__(self, name):当默认属性访问以AttributeError失败时调用,应返回(计算出的)属性值或抛AttributeError。若属性按正常机制找到则不会调用__getattr__(与__setattr__的刻意不对称,出于效率且否则__getattr__将无由访问实例其它属性)。想完全掌控可不在实例属性字典插入值(改存到另一对象),见__getattribute__。object.__getattribute__(self, name):无条件为实例属性访问实现;若类也定义__getattr__,除非__getattribute__显式调用它或抛AttributeError,否则__getattr__不会被调用。为避免无限递归,实现中应始终调用基类同名方法(如object.__getattribute__(self, name))来访问所需属性。注意:作为隐式调用结果查找特殊方法时,__getattribute__仍可能被绕过(见「特殊方法查找」)。object.__setattr__(self, name, value):属性赋值时调用(替代「把值存进实例字典」的正常机制)。若要赋给实例属性,应调用object.__setattr__(self, name, value)。object.__delattr__(self, name):与__setattr__类似,用于属性删除;仅在del obj.name有意义时才应实现。object.__dir__(self):调用dir时触发,须返回一个序列(dir会转成列表并排序)。
模块属性访问的定制:模块级 __getattr__ 接受属性名一个参数,正常查找未命中时被查找并调用,返回计算值或抛 AttributeError;模块级 __dir__ 接受零参、返回可访问名字字符串列表,存在时覆盖标准 dir 查找。更细粒度地可把模块对象的 __class__ 设为 types.ModuleType 子类。例如:
import sys
from types import ModuleType
class VerboseModule(ModuleType):
def __repr__(self):
return f'Verbose {self.__name__}'
def __setattr__(self, attr, value):
print(f'Setting {attr}...')
super().__setattr__(attr, value)
sys.modules[__name__].__class__ = VerboseModule
注意:模块 __getattr__ 与设置模块 __class__ 只影响「属性访问语法」的查找,不影响「直接访问模块全局变量」的查找。模块 __class__ 自 3.5 可写;__getattr__/__dir__ 模块属性为 3.7 新增。
描述符(Descriptors)
以下方法仅在「包含该方法的类(描述符类)的实例」出现在某个属主类的(或其某个父类的)类字典中时生效。属主类 __dict__ 中以该属性名为键的对象即「该属性」。
object.__get__(self, instance, owner):读取属主类或其实例的属性时调用。owner始终是属主类;instance为访问所经实例,经属主访问时为None。应返回值或抛AttributeError。object.__set__(self, instance, value):把属主类实例的属性设为新值时调用。object.__delete__(self, instance):删除该属性时调用。object.__set_name__(self, owner, name):属主类创建时、描述符被赋名为 name 时调用(3.6 新增)。
属性 __objclass__ 被 inspect 解释为「定义此对象的类」。对可调用对象,它可表明期望/要求以给定类型(或子类)实例作为第一个位置参数(CPython 对 C 实现的未绑定方法设置此属性)。
描述符的调用:默认属性访问行为是从对象字典取/设/删;例如 a.x 的查找链始于 a.__dict__['x'],再到 type(a).__dict__['x'],沿 type(a) 的基类(不含元类)继续。但若查到的值定义了 __get__/__set__/__delete__ 之一,Python 可能覆盖默认行为、转而调用描述符方法;具体优先级取决于定义了哪些描述符方法、以及它们如何被调用。以绑定 a.x 为起点,参数组装取决于 a:
- 直接调用:用户代码直接
x.__get__(a)。 - 实例绑定:
a.x变成type(a).__dict__['x'].__get__(a, type(a))。 - 类绑定:
A.x变成A.__dict__['x'].__get__(None, A)。 - Super 绑定:
a是super实例时,super(B, obj).m()在obj.__class__.__mro__中查找到紧随B之前的基类A,然后A.__dict__['m'].__get__(obj, obj.__class__)。
对实例绑定,描述符调用优先级取决于定义了哪些方法:描述符可任意组合 __get__/__set__/__delete__。若未定义 __get__,除非实例字典有值否则访问返回描述符本身。定义了 __set__/__delete__ 之一即「数据描述符」;都没定义则「非数据描述符」。通常数据描述符同时定义 __get__ 与 __set__,非数据描述符只有 __get__。定义了 __set__ 与 __get__ 的数据描述符总是覆盖实例字典中的重定义;而非数据描述符可被实例覆盖。Python 方法(含 staticmethod/classmethod)作为非数据描述符实现,因此实例可重定义/覆盖方法;而 property 作为数据描述符,实例无法覆盖其行为。
__slots__
__slots__ 允许显式声明数据成员(如属性),并拒绝创建 __dict__ 与 __weakref__(除非显式声明或父类提供)。相较 __dict__ 节省的空间可能很可观,属性查找速度也可能显著提升。object.__slots__ 类变量可被赋字符串、可迭代或字符串序列(实例所用的变量名),为声明的变量保留空间、防止自动为每个实例创建 __dict__/__weakref__。
使用要点:
- 从「无
__slots__」的类继承时,实例的__dict__/__weakref__始终可访问。 - 无
__dict__变量时,实例不能赋值__slots__未列出的新变量名(抛AttributeError);若需动态赋值,把'__dict__'加入序列。 - 无
__weakref__变量时,定义了__slots__的类不支持对其实例的弱引用;需要则加入'__weakref__'。 __slots__在类层面通过为每个变量名创建描述符实现,因此「类属性不能用作__slots__实例变量的默认值」(否则类属性会覆盖描述符赋值)。- 其作用不限于定义它的类:父类声明的
__slots__在子类可用;但子类若不再定义__slots__会得到__dict__/__weakref__(子类只应含「额外」的 slot 名)。 - 若某类定义了基类也定义的 slot,基类 slot 的实例变量将不可访问(除非从基类直接取其描述符),使程序含义未定义(未来可能加检查)。
- 非空
__slots__不适用于派生自「变长」内建类型(如int/bytes/tuple)的类。 - 任意非字符串可迭代都可赋给
__slots__,映射也可(但未来可能赋予键对应值特殊含义)。 __class__赋值仅在两个类有相同__slots__时才工作。- 多个带 slot 的父类的多重继承可用,但只允许一个父类有「由 slot 创建属性」(其余基类的 slot 布局必须为空),违反则抛
TypeError。
定制类的创建
类继承自另一类时,会在该类上调用 __init_subclass__。与类装饰器(只影响其装饰的那个类)不同,__init_subclass__ 只作用于「定义该方法的类的未来子类」。
object.__init_subclass__(cls):每当包含它的类被子类化时调用,cls 为新子类;若定义为普通实例方法会被隐式转为类方法。传给新类的关键字参数会被传入父类的__init_subclass__;为兼容其它使用者,应取出所需关键字参数并把其余传给基类:
class Philosopher:
def __init_subclass__(cls, default_name, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_name = default_name
class AustralianPhilosopher(Philosopher, default_name="Bruce"):
pass
默认实现 object.__init_subclass__ 不做任何事,但被带参调用时抛错。metaclass 提示会被类型机制消费,从不会被传给 __init_subclass__;实际元类可用 type(cls) 访问。(3.6 新增。)
元类(Metaclasses):默认用 type 构造类——类体在新命名空间执行,类名在本地绑定到 type(name, bases, namespace) 的结果。可在类定义行传 metaclass 关键字参数、或继承一个已含该参数的类来定制类创建。下例中 MyClass 与 MySubclass 都是 Meta 的实例:
class Meta(type):
pass
class MyClass(metaclass=Meta):
pass
class MySubclass(MyClass):
pass
类定义执行时依次:解析 MRO 条目 → 确定合适元类 → 准备类命名空间 → 执行类体 → 创建类对象。
- 解析 MRO 条目:若类定义中某基类不是
type的实例,则在其上查找__mro_entries__;若找到则以原基类元组调用之,其必须返回一个「将替代该基类」的类元组(可为空,表示忽略原基类)。 - 确定合适元类:无基类且无显式元类则用
type;显式元类非type实例则直接用作元类;给定type实例或定义了基类则用「最派生元类」。最派生元类从显式元类与所有指定基类的元类(type(cls))中选出,是「所有这些候选元类的子类型」者;无满足者则类定义以TypeError失败。 - 准备类命名空间:若元类有
__prepare__,以namespace = metaclass.__prepare__(name, bases, **kwds)调用(附加关键字参数来自类定义);否则初始化为空有序映射。 - 执行类体:近似
exec(body, globals(), namespace)。关键区别是词法作用域允许类体(含方法)在类定义发生在函数内时引用当前及外层作用域的名字。但即便类定义在函数内,类内方法仍看不到类作用域定义的名字——类变量须通过实例/类方法的第一参数、或隐式词法__class__引用访问。 - 创建类对象:调用
metaclass(name, bases, namespace, **kwds)创建。此对象即零参super所引用的对象;__class__是编译器在类体方法引用__class__或super时创建的隐式闭包引用,使零参super能基于词法作用域识别正在定义的类(而做出当前调用的类/实例则基于方法第一参数识别)。CPython 3.6+ 会把__class__cell 作为__classcell__项传给元类,须传播到type.__new__调用;3.6 遗漏会DeprecationWarning,3.8 起RuntimeError。使用默认元类type或最终调用type.__new__的元类,创建类对象后还会执行额外定制:先收集命名空间中定义了__set_name__的描述符,再逐个调用其__set_name__(传入正被定义的类与该描述符名),最后在新类 MRO 上的直接父类上调用__init_subclass__钩子。类对象创建后交给类装饰器(如有),其结果在本地绑定为所定义类。由type.__new__创建新类时,作为命名空间参数的对象会被拷贝到新有序映射、原对象丢弃,新拷贝被包装为只读代理成为类对象的__dict__。
元类的潜在用途广泛:枚举、日志、接口检查、自动委托、自动属性创建、代理、框架、自动资源加锁/同步等。
定制实例与子类检查
以下方法用于覆盖 isinstance/issubclass 的默认行为;元类 abc.ABCMeta 实现了它们以允许把抽象基类(ABC)作为「虚拟基类」加入任意类/类型(含内建类型)乃至其它 ABC。
class.__instancecheck__(self, instance):定义时用于实现isinstance(instance, class),返回是否应将 instance 视为(直接或间接)实例。class.__subclasscheck__(self, subclass):定义时用于实现issubclass(subclass, class),返回是否应将 subclass 视为(直接或间接)子类。
注意这些方法在「类的类型(元类)」上查找,不能作为类方法定义在实际类里——这与在实例上查找特殊方法一致,只是这里实例本身是类。
模拟泛型类型
可通过定义特殊方法实现 PEP 484 的泛型类语法(如 List[int]):
object.__class_getitem__(cls, key):返回表示「由 key 中类型参数特化的泛型类」的对象。在类对象上查找,定义于类体内时隐式为类方法。此机制主要为静态类型提示保留,其它用法不推荐。
模拟可调用对象
object.__call__(self[, args...]):实例被「当作函数调用」时调用;定义了它,x(arg1, arg2, ...)就是x.__call__(arg1, arg2, ...)的简写。
模拟容器类型
以下方法用于实现容器对象。容器通常是序列(list/tuple)或映射(dict),也可表示其它容器。序列允许的键应是满足 0 <= k < N(N 为长度)的整数或切片对象;映射建议提供 keys/values/items/get/clear/setdefault/pop/popitem/copy/update(行为类似标准 dict)——collections.abc.MutableMapping 可帮助从 __getitem__/__setitem__/__delitem__/keys 这组基础方法生成它们;可变序列应提供 append/count/index/extend/insert/pop/remove/reverse/sort;序列类型应通过 __add__/__radd__/__iadd__/__mul__/__rmul__/__imul__ 实现「加(拼接)」与「乘(重复)」,不应定义其它数值运算符。建议映射与序列都实现 __contains__(映射查键、序列查值)与 __iter__(映射同 keys、序列迭代值)。
object.__len__(self):实现len,返回>= 0的整数;未定义__bool__且__len__返回 0 的对象在布尔上下文中为假。实现细节:CPython 中长度须至多sys.maxsize,更大时len等特性可能OverflowError;要避免真值测试抛OverflowError,对象必须定义__bool__。object.__length_hint__(self):实现operator.length_hint,返回估计长度(可大于或小于实际长度,须>= 0整数);纯属优化,非正确性必需(3.4 新增)。- 切片只由三个方法完成:
a[1:2] = b被翻译为a[slice(1, 2, None)] = b,缺省切片项总填None。 object.__getitem__(self, key):实现self[key]。序列类型接受的键应是整数与切片对象,负索引的特殊解释由__getitem__自行处理;键类型不当抛TypeError,值超出索引集(经负值特殊解释后)抛IndexError;映射类型键缺失抛KeyError。for循环期望非法索引抛IndexError以正确探测序列末尾。object.__setitem__(self, key, value):实现self[key]赋值;与__getitem__同注意。映射仅在支持改值/加键时实现,序列仅在元素可替换时实现;非法 key 抛与__getitem__相同异常。object.__delitem__(self, key):实现self[key]删除;映射仅在支持删键、序列仅在支持移除元素时实现;非法 key 同__getitem__。object.__missing__(self, key):由dict.__getitem__在 dict 子类的键不在字典中实现self[key]时调用。object.__iter__(self):需要迭代器时为容器调用,应返回可遍历所有对象的新迭代器;映射应迭代键。迭代器对象也须实现它且须返回自身。object.__reversed__(self):若存在则被reversed用于反向迭代,应返回以逆序迭代所有对象的新迭代器。未提供时reversed回退到序列协议(__len__与__getitem__);支持序列协议的对象仅当能提供比reversed更高效的实现时才应提供__reversed__。- 成员测试(
in/not in)通常实现为序列迭代,但容器可提供更高效的特殊方法且无需是序列:object.__contains__(self, item)实现成员测试,项在 self 中返回真;映射对象应考虑键而非值或键值对。未定义__contains__时,成员测试先尝试__iter__迭代,再回退到旧序列协议__getitem__。
模拟数值类型
以下方法用于模拟数值对象;某类数字不支持的操作(如非整数的位运算)应留空(不定义)。
- 二元算术:
__add__/__sub__/__mul__/__matmul__/__truediv__/__floordiv__/__mod__/__divmod__/__pow__/__lshift__/__rshift__/__and__/__xor__/__or__分别实现+ - * @ / // %、divmod、pow、**、<< >> & ^ |。求x + y(x 为定义了__add__的实例)会调用x.__add__(y);__divmod__应等价于__floordiv__+__mod__,与__truediv__无关;__pow__若要支持三元pow应定义接受可选第三参。方法若不支持给定操作数的操作,应返回NotImplemented。 - 反射(换参)操作:
__radd__/__rsub__/__rmul__/__rmatmul__/__rtruediv__/__rfloordiv__/__rmod__/__rdivmod__/__rpow__/__rlshift__/__rrshift__/__rand__/__rxor__/__ror__仅在左操作数不支持对应操作「且操作数类型不同」时调用。例如求x - y,若x.__sub__(y)返回NotImplemented且 y 定义了__rsub__,则调用y.__rsub__(x)。三元pow不会尝试__rpow__(强转规则会变得过于复杂)。若右操作数类型是左操作数类型的子类、且该子类提供了反射方法,则它会先于左操作数的非反射方法被调用(允许子类覆盖祖先操作)。 - 增强赋值:
__iadd__/__isub__/__imul__/__imatmul__/__itruediv__/__ifloordiv__/__imod__/__ipow__/__ilshift__/__irshift__/__iand__/__ixor__/__ior__实现+= -= *= @= //= %= **= <<= >>= &= ^= |=;应尝试原地修改 self 并返回结果(可以是但不必是 self)。未定义时回退到普通方法:若 x 定义了__iadd__,x += y等价x = x.__iadd__(y);否则按x + y的求值顺序考虑x.__add__(y)与y.__radd__(x)。某些情况下增强赋值可能导致「意料之外」的错误,但这是数据模型的一部分。 - 一元:
__neg__/__pos__/__abs__/__invert__实现-、+、abs、~。 - 类型转换:
__complex__/__int__/__float__实现complex/int/float,应返回相应类型的值。 object.__index__(self):实现operator.index,以及 Python 需要「无损地把数值对象转成整数对象」时(切片、bin/hex/oct)。存在此方法即表明该数值对象是整型,须返回整数。为保持整型类内聚,定义__index__时应同时定义__int__,且二者返回相同值。object.__round__(self, [,ndigits])/__trunc__/__floor__/__ceil__:实现round与math.trunc/math.floor/math.ceil;未向__round__传 ndigits 时这些方法都应返回截断到numbers.Integral(通常是int)的值。若未定义__int__,int会回退到__trunc__。
with 语句的上下文管理器
上下文管理器是定义「执行 with 语句时建立的运行时上下文」的对象,负责进入与离开目标运行时上下文。典型用途:保存/恢复各种全局状态、加/解锁资源、关闭打开的文件等。上下文管理器通常经 with 语句调用,也可直接调用其方法。
object.__enter__(self):进入该对象相关运行时上下文;with会把它返回值绑定到as子句的目标(若有)。object.__exit__(self, exc_type, exc_value, traceback):离开该对象相关运行时上下文;三个参数描述导致上下文退出的异常(无异常则三者皆None)。若提供了异常且方法希望抑制它(阻止传播),应返回真值,否则该异常在离开方法后被正常处理。__exit__不应重新抛出传入的异常——那是调用者的职责。
特殊方法查找
对自定义类,隐式调用特殊方法只有「定义在对象的类型上」(而非对象实例字典里)才有保证正确工作。这正是以下代码抛异常的原因:
>>> class C:
... pass
...
>>> c = C()
>>> c.__len__ = lambda: 5
>>> len(c)
TypeError: object of type 'C' has no len()
其原理在于 __hash__、__repr__ 等这类特殊方法由所有对象(包括类型对象)实现。若隐式查找采用常规查找过程,在「类型对象本身」上调用就会失败:
>>> 1 .__hash__() == hash(1)
True
>>> int.__hash__() == hash(int)
TypeError: descriptor '__hash__' of 'int' object needs an argument
这种「以未绑定方法方式错误调用类」的尝试有时被称为「元类混淆(metaclass confusion)」,可通过「查找特殊方法时绕过实例」来避免:
>>> type(1).__hash__(1) == hash(1)
True
>>> type(int).__hash__(int) == hash(int)
True
除为正确性绕过实例属性外,隐式特殊方法查找通常还会绕过「对象的元类的」__getattribute__:
>>> class Meta(type):
... def __getattribute__(*args):
... print("Metaclass getattribute invoked")
... return type.__getattribute__(*args)
...
>>> class C(object, metaclass=Meta):
... def __len__(self):
... return 10
... def __getattribute__(*args):
... print("Class getattribute invoked")
... return object.__getattribute__(*args)
...
>>> c = C()
>>> c.__len__() # 经实例显式查找
Class getattribute invoked
10
>>> type(c).__len__(c) # 经类型显式查找
Metaclass getattribute invoked
10
>>> len(c) # 隐式查找
10
以这种方式绕过 __getattribute__ 机制给解释器提供了显著的性能优化空间,代价是特殊方法处理上的灵活性受限(特殊方法必须设置在类对象本身上,才能被解释器一致地调用)。
四、协程(Coroutines)
可等待对象(Awaitable)通常实现 __await__ 方法;async def 函数返回的协程对象即可等待。types.coroutine 或 asyncio.coroutine 装饰的生成器返回的生成器迭代器对象也可等待,但它们不实现 __await__。object.__await__(self) 必须返回一个迭代器,用于实现可等待对象(如 asyncio.Future 用它以兼容 await)。
协程对象是可等待对象,其执行可通过调用 __await__ 并迭代结果来控制。协程执行完返回时迭代器抛 StopIteration,其 value 属性保存返回值;协程抛异常则由迭代器传播;协程不应直接抛出未处理的 StopIteration。协程还支持与生成器类似(但不支持直接迭代)的方法:
coroutine.send(value):开始或恢复执行;value 为None时等价于推进__await__返回的迭代器,否则委托给使协程挂起的迭代器的generator.send。coroutine.throw(type[, value[, traceback]]):在协程中抛出指定异常,委托给挂起迭代器的generator.throw(若有),否则在挂起点抛出;未被协程捕获则传播回调用者。coroutine.close():让协程清理并退出——若挂起则先委托其挂起迭代器的generator.close(若有),再在挂起点抛GeneratorExit使其立即清理,最后标记为已执行完(即使从未开始)。协程即将被销毁时会自动经此流程关闭。(自 3.5.2 起,await 同一协程多于一次是RuntimeError。)
异步迭代器可在其 __anext__ 中调用异步代码,可用于 async for:
object.__aiter__(self):必须返回异步迭代器对象。object.__anext__(self):必须返回产生下一值的可等待对象,迭代结束时抛StopAsyncIteration。
class Reader:
async def readline(self):
...
def __aiter__(self):
return self
async def __anext__(self):
val = await self.readline()
if val == b'':
raise StopAsyncIteration
return val
自 3.7 起,__aiter__ 必须返回异步迭代器对象(返回其它将 TypeError)。
异步上下文管理器是能在 __aenter__/__aexit__ 中挂起的上下文管理器,可用于 async with:object.__aenter__(self) 语义同 __enter__ 但须返回可等待对象;object.__aexit__(self, exc_type, exc_value, traceback) 语义同 __exit__ 但须返回可等待对象。
class AsyncContextManager:
async def __aenter__(self):
await log('entering context')
async def __aexit__(self, exc_type, exc, tb):
await log('exiting context')
五、在 KBEngine 中的落地:内嵌解释器与 C++/Python 桥接
上文的数据模型并非与 KBEngine 无关的理论章节,而是引擎直接依赖的运行基础,可从源码结构得到印证。
内嵌解释器版本:KBEngine 将 CPython 源码随仓库携带,patchlevel.h 中 PY_MAJOR_VERSION/PY_MINOR_VERSION/PY_MICRO_VERSION 分别为 3/7/3,确认内嵌解释器为 Python 3.7.3;因此本篇所依据的 3.7 语义(如 3.7 新增的模块 __getattr__/__dir__、可显式实例化的 traceback、__aiter__ 必须返回异步迭代器等)正是引擎运行时实际生效的版本行为。
构建产物:从 kbe/src/lib/Makefile 可见,构建会 cd python && make 编译出 libpython3.7m.a,并复制为 kbe/src/lib/libs/libpython.a,同时把 python/Lib 拷贝到 kbe/res/scripts/common/Lib、把 build/lib.* 动态库拷贝到 kbe/res/scripts/common/lib-dynload/。换言之,服务端组件运行时链接的就是这份内嵌的 CPython 及其标准库,脚本逻辑(实体、属性、远程方法等)都运行在这个数据模型之上。
用映射/序列协议把 C++ 对象暴露给脚本:引擎的 pyscript 桥接层正是「模拟容器类型」一章的活例子。pyscript/map.h 声明 static PyMappingMethods mappingMethods; 与 static PySequenceMethods mappingSequenceMethods;,并把 __py_has_key/__py_keys/__py_values/__py_items/__py_update/__py_get 以及 mp_subscript/mp_ass_subscript/mp_length/mp_keyiter/mp_iternextkey 等 C 方法填充进映射协议,再叠加 seq_contains 等序列方法,使引擎内部对象在 Python 侧表现得像一个 dict(键索引、取值/赋值、迭代键、in 成员测试等),并对外暴露 keys/values/items/get/update 等字典式方法。pyscript/sequence.h 同理声明 PySequenceMethods seqMethods; 与 PyMappingMethods seqMapping;,实现 seq_length/seq_item/seq_slice/seq_ass_item/seq_ass_slice/seq_contains/seq_concat/seq_repeat/seq_inplace_concat 等,让引擎序列对象在脚本中像 list。这正对应文中「映射应提供 keys/values/items/get/... 」「可变序列应提供 append/.../contains」以及 PyMappingMethods/PySequenceMethods 插槽的机制。
二进制序列化与数据变更通知:map.h 还定义了 virtual void onDataChanged(PyObject* key, PyObject* value, bool isDelete) 用于「数据改变通知」,并 #include "pickler.h"(见 pyscript/map.h 顶部);pickler 负责对 Python 对象做序列化/反序列化(把属性值编解码到网络/数据库往返)。pyscript/pystruct.h 中的 PyStruct 则静态封装了 struct.pack/struct.unpack,用于按格式串做二进制打包与解包——这些都属于「把 Python 值/字节对象在引擎 C++ 与脚本之间高效搬运」的底层工具。
说明:以上 KBEngine 侧结论均来自 patchlevel.h、kbe/src/lib/Makefile 与 pyscript 头文件的直接证据;关于「具体哪个服务端进程在何时调用 pickler/PyStruct」这类调用链细节,仓库中未在本次阅读范围内给出可直接引用的完整代码,故仅作「从源码结构看」的表述,不作超出证据的断言。
小结
- 对象有身份、类型、值三维度;可变性由类型决定;CPython 用引用计数 + 循环垃圾延迟回收,不要依赖立即终结化。
- 标准类型层级涵盖单值(None/NotImplemented/Ellipsis)、数字(int/bool/float/complex)、序列(str/tuple/bytes 不可变,list/bytearray 可变)、集合(set/frozenset)、映射(dict)、可调用(函数/方法/生成器/协程/类/实例/模块)与内部类型(code/frame/traceback/slice/staticmethod/classmethod)。
- 「运算符重载」= 在类上定义特殊方法名;富比较、哈希(含加盐与
__eq__/__hash__配套规则)、属性访问(__getattr__/__getattribute__/__setattr__/__delattr__)、描述符(数据/非数据优先级)、__slots__、__init_subclass__与元类(MRO 解析 → 元类确定 →__prepare__→ 执行类体 → 创建类对象并回调__set_name__/__init_subclass__)构成类定制主干。 - 容器/数值/上下文/可调用各有成组的特殊方法;隐式特殊方法查找绕过实例与元类的
__getattribute__,特殊方法必须设在类对象上。 - 协程/异步迭代器/异步上下文管理器(
__await__/__aiter__/__anext__/__aenter__/__aexit__)是 3.5+ 的异步数据模型。
对 KBEngine 脚本作者而言,理解这套数据模型能直接解释「引擎对象为何在脚本中像 dict/list、属性变更如何被通知、值如何被序列化往返」,从而写出与引擎桥接层(pyscript/map.h、pyscript/sequence.h、pyscript/pystruct.h)契合、且符合内嵌 Python 3.7.3 语义的服务端逻辑。