大多数 Python 开发者只知道__len__ 对应 len()、__add__ 对应 +。事实上Python 数据模型,有一套完整、规范、官方定义的协议。它让我们自定义的对象,可以融入 Python 语言本身,真值判断、哈希、迭代、上下文管理、属性访问、内存布局…… 全部由它控制。要想用好它,必须理解:CPython 到底在何时、以何种顺序、为何调用这些方法。
双下划线方法是由Python 解释器调用的,而不是我们的业务代码直接调用。当我们写 len(obj),解释器实际执行 type(obj).__len__(obj)。它不会直接调用 obj.__len__(),而是通过类去查找,不是通过实例。
class MyClass: passobj = MyClass()obj.__len__ = lambda: 42 # 没用!len(obj) # 直接报错
真值、相等、哈希:三者是强绑定关系,这是最容易写出隐蔽 Bug 的地方。在真值判断时,__bool__优先,没有__bool__,就用__len__ 长度为0即为假,都没有则为真。class Container: def __len__(self): return len(self.items)c = Container([])if not c: print("empty") # 会执行
接下来我们看看相等和哈希,__eq__定义值相等,相等的对象,必须拥有相同的哈希值,一旦定义了__eq__,python会自动把__hash__设定为None,对象变成了不可哈希。classPoint: def __eq__(self, other): ...p = Point(){p} # 报错:不可哈希
def __hash__(self): return hash((self.x, self.y))
当我们些a<b时,它会先尝试type(a).__lt__(a,b),如果返回NotImplemented python会自动尝试反射操作,type(b).__gt__(b,a) 这就是不同类型可以互相比较的底层原理。2.反射: 3+v (对象在右边) -> __radd__class Vector: def __add__(self, other): ... def __radd__(self, other): return self.__add__(other) def __iadd__(self, other): # 原地修改 self.x += other.x return self # 必须 return self
__radd__让自定义类型能和内置类型无缝配合。1. __getattribute__ 所有属性访问都会触发,几乎不需要重写2.__getattr__ 只有查找失败时才会触发,适合懒加载、代理、动态属性class LazyLoader: def __getattr__(self, name): # 找不到时才执行
3. __setattr__ 所有赋值都会触发,必须用super()赋值,否则无限递归def __setattr__(self, name, value): super().__setattr__(name, value)
另外,如果想实现一个标准序列 / 容器,只需要实现__len__和__getitem__,我们看个例子:class Fibonacci: def __len__(self): ... def __getitem__(self, index): ...fib[10] # 可用fib[2:5] # 可用10 in fib # 可用for n in fib: # 可用
映射类型(dict 风格)继承 MutableMapping,实现 5 个方法,就能获得 keys()/values()/items() 等全部功能。__slots__是数据模型的一部分,它的作用是禁止实例创建__dict__,使用固定内存布局,内存占用大幅降低class Point: __slots__ = ("x", "y")
需要注意的是,子类必须也定义slot,否则会重写生成dict,优化失效。最后,希望这篇内容能帮我们更深入的了解python的数据模型。