KBEngine 内嵌 Python 3.7.3 数据模型详解:对象、类型层级与特殊方法机制

原创2026-10-18 16:39:151,894 阅读
文章标签:游戏开发后端

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 语义的服务端逻辑。

登录后查看全文
kbengine