定制类
Python 的很多语法最终都会转换为对特殊方法的调用。写 len(value),Python 会寻找 value.__len__();写 value[0],会调用 value.__getitem__(0);执行 with resource:,会调用 __enter__() 和 __exit__()。
这些以双下划线包围的方法常被称为特殊方法或魔术方法。它们不是为了炫技,而是让自定义类遵守 Python 已经存在的协议。遵守协议后,自己的对象就能自然地参与 len()、for、in、比较、算术、with 等语言操作。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 为对象提供有用的字符串表示和调试信息;
- 实现序列、映射、可调用对象等常见协议;
- 正确处理相等性、排序、哈希和
NotImplemented; - 定制运算符、真值判断和上下文管理;
- 区分
__getattr__()、__getattribute__()与__setattr__(); - 根据实际需求选择特殊方法,避免过度定制。
先看一个完整例子
下面的 Playlist 只保存歌曲名称,却能像一个有长度、可迭代、支持下标和 in 的容器一样使用:
class Playlist:
def __init__(self, name: str, songs: list[str] | None = None):
self.name = name
self._songs = list(songs or [])
def __repr__(self) -> str:
return f"Playlist({self.name!r}, songs={self._songs!r})"
def __str__(self) -> str:
return f"{self.name}({len(self)} 首歌)"
def __len__(self) -> int:
return len(self._songs)
def __getitem__(self, index: int | slice) -> str | list[str]:
return self._songs[index]
def __iter__(self):
return iter(self._songs)
def __contains__(self, song: str) -> bool:
return song in self._songs
def add(self, song: str) -> None:
self._songs.append(song)
playlist = Playlist("通勤", ["A", "B", "C"])
print(playlist) # 通勤(3 首歌)
print(repr(playlist)) # Playlist('通勤', songs=['A', 'B', 'C'])
print(len(playlist)) # 3
print(playlist[1]) # B
print(playlist[1:]) # ['B', 'C']
print("A" in playlist) # True
print(list(playlist)) # ['A', 'B', 'C']
这些特殊方法组成的是一组小协议。实现 __len__() 并不意味着必须实现所有其他方法;应根据类真正支持的行为逐项添加。
__str__()、__repr__() 和格式化
面向用户的 __str__()
str(value) 和 print(value) 会优先使用 __str__()。它适合输出简洁、可读的业务描述:
class User:
def __init__(self, name: str, role: str):
self.name = name
self.role = role
def __str__(self) -> str:
return f"{self.name}({self.role})"
print(User("小林", "管理员")) # 小林(管理员)
面向开发者的 __repr__()
repr(value)、交互式解释器回显以及容器中的元素通常使用 __repr__()。它应该尽量明确,方便调试;如果可能,返回的文本应接近能重新构造对象的表达式:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Point(x={self.x!r}, y={self.y!r})"
point = Point(2, 3)
print(str(point)) # 没有 __str__ 时,会使用 __repr__
print([point]) # [Point(x=2, y=3)]
如果没有定义 __str__(),Python 会回退到 __repr__();因此至少提供一个有用的 __repr__() 通常是很好的默认选择。两个方法都必须返回字符串,不能返回其他对象。
__format__() 与格式说明符
需要支持 format(value, spec) 或 f-string 格式说明符时,可以实现 __format__():
class Percentage:
def __init__(self, value: float):
self.value = value
def __format__(self, spec: str) -> str:
if spec == "percent":
return f"{self.value * 100:.1f}%"
return format(self.value, spec)
ratio = Percentage(0.875)
print(f"{ratio:percent}") # 87.5%
格式化逻辑应保持轻量,不要在 __str__() 或 __format__() 中执行网络请求等副作用操作。
让对象像容器一样工作
__len__()、__bool__() 和真值
实现 __len__() 后,len(value) 返回非负整数;在没有 __bool__() 时,Python 也会使用长度判断对象真假:
class Basket:
def __init__(self, items: list[str]):
self.items = list(items)
def __len__(self) -> int:
return len(self.items)
empty = Basket([])
full = Basket(["书"])
print(bool(empty)) # False
print(bool(full)) # True
如果真值不应由长度决定,可以单独实现 __bool__()。例如,连接对象可以根据“是否已连接”判断,而不是根据内部缓冲区长度判断。__len__() 返回负数或非整数会触发 TypeError。
__getitem__() 支持下标和切片
下标表达式 value[key] 会调用 __getitem__(key)。key 可能是整数、字符串、切片对象,甚至是元组:
class History:
def __init__(self, events: list[str]):
self._events = list(events)
def __getitem__(self, key: int | slice) -> str | list[str]:
return self._events[key]
history = History(["登录", "搜索", "退出"])
print(history[0]) # 登录
print(history[-1]) # 退出
print(history[:2]) # ['登录', '搜索']
如果类只支持整数下标,就应在收到其他类型时抛出清晰的 TypeError。越界行为通常遵循内置序列的约定,抛出 IndexError。
迭代:__iter__() 与 __next__()
for item in value 会调用 iter(value)。最常见的做法是让 __iter__() 返回内部容器的迭代器:
class Numbers:
def __init__(self, values: list[int]):
self.values = list(values)
def __iter__(self):
return iter(self.values)
for number in Numbers([1, 2, 3]):
print(number)
只有对象本身就是一次性的迭代器时,才需要同时实现 __next__(),并在结束时抛出 StopIteration:
class Countdown:
def __init__(self, start: int):
self.current = start
def __iter__(self):
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
可重复遍历的容器应让每次 iter(container) 都返回新的迭代器,不要把容器本身做成有状态的迭代器。
__contains__() 与 in
item in value 优先使用 __contains__(item)。如果没有它,Python 可能尝试遍历对象,或依赖序列式的 __getitem__():
class PermissionSet:
def __init__(self, permissions: set[str]):
self.permissions = set(permissions)
def __contains__(self, permission: str) -> bool:
return permission in self.permissions
allowed = PermissionSet({"read", "write"})
print("read" in allowed) # True
显式实现通常更快,也能表达与默认遍历不同的语义。
映射协议
想让对象像字典一样支持按键读写,可以实现 __getitem__()、__setitem__()、__delitem__() 和 __iter__() 等方法:
class Config:
def __init__(self):
self._values: dict[str, str] = {}
def __getitem__(self, key: str) -> str:
return self._values[key]
def __setitem__(self, key: str, value: str) -> None:
if not key:
raise ValueError("配置名不能为空")
self._values[key] = value
def __delitem__(self, key: str) -> None:
del self._values[key]
def __iter__(self):
return iter(self._values)
def __len__(self) -> int:
return len(self._values)
config = Config()
config["theme"] = "dark"
print(config["theme"])
如果需要完整的映射行为,继承 collections.abc.MutableMapping 并实现它要求的最小方法,通常比手动猜测所有细节更可靠。
比较、相等和哈希
__eq__() 应比较值,而不是身份
默认情况下,自定义对象的相等性通常接近身份比较。值对象可以实现 __eq__():
class Coordinate:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, Coordinate):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
print(Coordinate(1, 2) == Coordinate(1, 2)) # True
返回 NotImplemented 表示“我不支持与这个类型比较”,让 Python 有机会尝试右侧类型的反向实现或得出合理的结果。不要把不兼容类型一律当成 False,否则可能掩盖类型设计错误。
其他排序方法
可以实现 __lt__()、__le__()、__gt__() 和 __ge__()。如果比较规则有明确的全序关系,functools.total_ordering 可以根据少数方法补齐其他方法,但它会增加调用层次,性能敏感代码可以直接完整实现:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int):
self.major = major
self.minor = minor
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) < (other.major, other.minor)
print(Version(2, 0) > Version(1, 9)) # True
__hash__() 与可变性
哈希对象必须满足:如果 a == b,就必须有 hash(a) == hash(b)。因此,参与 set 或作为 dict 键的对象,其影响相等性的状态不能在放入容器后改变。
实现了自定义 __eq__() 的可变类通常应保持不可哈希。Python 往往会自动把 __hash__ 设为 None;也可以显式写出:
class MutablePoint:
__hash__ = None
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, MutablePoint):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
如果对象真正不可变,才可以根据相等字段实现哈希:
class FrozenPoint:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
def __eq__(self, other: object) -> bool:
if not isinstance(other, FrozenPoint):
return NotImplemented
return (self._x, self._y) == (other._x, other._y)
def __hash__(self) -> int:
return hash((self._x, self._y))
仅仅把 __hash__ 写出来,并不能让一个会变化的对象安全地成为字典键。
定制运算符
二元运算和 NotImplemented
a + b 会尝试 a.__add__(b),必要时还会尝试 b.__radd__(a)。不支持对方类型时,应返回 NotImplemented:
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other: object):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
print(Vector(1, 2) + Vector(3, 4)) # Vector(4, 6)
返回 NotImplemented 与直接抛出 NotImplementedError 完全不同:前者是二元运算协议中的信号,后者通常表示子类必须实现某个方法。
反向方法和就地方法
常见配对包括 __add__() / __radd__()、__mul__() / __rmul__()。就地运算 a += b 会优先尝试 __iadd__();如果没有,Python 会回退到普通加法并重新绑定变量:
class Counter:
def __init__(self, value: int = 0):
self.value = value
def __iadd__(self, amount: int):
self.value += amount
return self
counter = Counter(1)
counter += 2
print(counter.value) # 3
可变对象的 __iadd__() 通常原地修改并返回 self;不可变对象则应创建并返回新对象。不要在就地方法中返回 None,否则 counter += 2 会把变量绑定为 None。
让对象可调用
实现 __call__() 后,实例可以像函数一样调用:
class Multiplier:
def __init__(self, factor: float):
self.factor = factor
def __call__(self, value: float) -> float:
return value * self.factor
double = Multiplier(2)
print(double(5)) # 10
print(callable(double)) # True
可调用对象适合保存函数之外的配置或状态,例如策略、验证器、适配器和回调。若对象只有一个简单操作、没有额外状态,普通函数通常更清楚。
上下文管理器
__enter__() 和 __exit__()
with value: 需要对象实现上下文管理协议:
class Transaction:
def __enter__(self):
print("开始事务")
return self
def __exit__(self, exc_type, exc_value, traceback) -> bool:
if exc_type is None:
print("提交事务")
else:
print("回滚事务")
return False # 不吞掉异常
with Transaction():
print("执行操作")
__enter__() 的返回值会绑定到 as 后面的变量。__exit__() 收到异常类型、异常对象和 traceback;返回真值会抑制异常,返回假值或 None 会让异常继续传播。除非确实完成了异常处理,否则不要返回 True。
contextlib 往往更简洁
只有进入和退出逻辑、没有复杂状态时,contextlib.contextmanager 可以把生成器包装成上下文管理器:
from contextlib import contextmanager
@contextmanager
def transaction():
print("开始事务")
try:
yield
except Exception:
print("回滚事务")
raise
else:
print("提交事务")
类形式适合需要暴露更多状态或实现异步协议的场景;简单资源边界可以优先考虑 contextmanager。
定制属性访问
__getattr__():普通查找失败后的兜底
只有在正常属性查找没有找到名称时,Python 才会调用 __getattr__(self, name):
class Settings:
def __init__(self, values: dict[str, str]):
self._values = values
def __getattr__(self, name: str) -> str:
try:
return self._values[name]
except KeyError as error:
raise AttributeError(name) from error
settings = Settings({"theme": "dark"})
print(settings.theme) # dark
# settings.language # AttributeError: language
实现 __getattr__() 时,找不到属性应抛出 AttributeError,这样 hasattr()、IDE 和其他反射工具才能得到正确结果。它适合代理、延迟加载和兼容旧名称。
__getattribute__():每次读取都会经过
__getattribute__() 会拦截几乎所有属性读取,功能强但很容易无限递归:
class Logged:
def __getattribute__(self, name: str):
print(f"读取 {name}")
return object.__getattribute__(self, name)
在它内部访问实例属性时,应使用 object.__getattribute__(self, name) 或 super().__getattribute__(name),不要再次写 self.name。只有日志、代理、权限检查等确有需求时才使用它;普通动态属性通常用 __getattr__() 更安全。
__setattr__():拦截赋值
想在所有赋值时做校验,可以实现 __setattr__(),但写入内部状态时必须绕过自身:
class Positive:
def __setattr__(self, name: str, value: object) -> None:
if name == "count" and (not isinstance(value, int) or value < 0):
raise ValueError("count 必须是非负整数")
object.__setattr__(self, name, value)
counter = Positive()
counter.count = 1
如果只需要控制一个或几个字段,@property 通常更明确;__setattr__() 会影响类的全部属性,范围太大时容易产生副作用。
对象创建与生命周期
__new__() 负责创建,__init__() 负责初始化
调用类时,Python 先调用 __new__() 获取实例,再调用 __init__() 初始化它。普通类只需实现 __init__();不可变类型、单例或实例缓存等场景才可能需要 __new__():
class AlwaysSame:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
first = AlwaysSame()
second = AlwaysSame()
print(first is second) # True
如果 __new__() 返回的对象不是当前类的实例,Python 通常不会调用对应的 __init__()。自定义 __new__() 前应先确认是否真的需要改变实例创建过程。
谨慎使用 __del__()
__del__() 可能在对象被垃圾回收时调用,但执行时机不稳定,解释器退出阶段模块全局变量也可能已经消失。循环引用、异常和进程终止都会让它不适合作为可靠的资源清理机制。
文件、锁、数据库连接等资源应使用 with 或显式的 close(),不要依赖 __del__() 完成关键清理。
用抽象基类表达协议
如果希望明确要求子类提供容器行为,可以继承 collections.abc 中的抽象基类:
from collections.abc import Sequence
class Names(Sequence[str]):
def __init__(self, values: list[str]):
self._values = list(values)
def __len__(self) -> int:
return len(self._values)
def __getitem__(self, index: int | slice):
return self._values[index]
names = Names(["小林", "小周"])
print(names[0])
print(names.index("小周")) # Sequence 提供的混入方法
Sequence 根据 __len__() 和 __getitem__() 提供一些通用行为,并把对象标记为序列协议的一员。使用标准抽象基类可以减少重复代码,也能让 isinstance() 检查更有意义。
定制类的常见误区
直接调用特殊方法和使用内置语法并不总是等价
日常代码应写 len(value)、iter(value)、str(value),而不是直接调用 value.__len__()。内置函数会遵守协议的边界行为,并可能处理继承、回退和错误类型。直接调用特殊方法通常只适合测试或调试。
随意实现一半协议
实现 __getitem__() 并不自动意味着对象支持完整列表语义。要明确是否支持切片、负索引、赋值、删除、迭代、包含判断和长度,并在文档和测试中保持一致。
忘记返回正确的特殊值
__repr__()、__str__()、__format__()必须返回字符串;__len__()必须返回非负整数;__iter__()必须返回迭代器;__eq__()等比较方法可以返回NotImplemented;__exit__()返回真值会吞掉异常;__iadd__()等就地运算必须返回对象或新值。
把所有行为都塞进特殊方法
特殊方法会让对象融入语言语法,但也隐藏了执行逻辑。复杂业务动作应保留清晰命名的普通方法;特殊方法只负责实现自然、稳定且符合 Python 约定的协议。
选择实现哪些特殊方法
可以按调用方真正需要的语法来决定:
- 需要日志和调试输出:实现
__repr__(),必要时再实现__str__()。 - 需要像容器一样使用:根据需求实现
__len__()、__getitem__()、__iter__()、__contains__()。 - 需要值语义:实现
__eq__(),再根据可变性决定是否实现__hash__()。 - 需要排序:实现合适的比较方法,并明确不支持的类型返回
NotImplemented。 - 需要算术语法:实现对应运算和反向运算,测试混合类型行为。
- 需要资源边界:实现上下文管理协议,或使用
contextlib。 - 需要动态代理:优先考虑
__getattr__(),谨慎使用__getattribute__()。
小结
- 特殊方法是 Python 数据模型协议的入口,让自定义对象自然参与内置语法;
__str__()面向用户,__repr__()面向调试,至少应提供清晰的__repr__();__len__()、__getitem__()、__iter__()和__contains__()可以组合出容器行为;- 自定义相等性时要处理
NotImplemented,并让哈希与不可变性保持一致; - 运算、可调用对象、上下文管理和属性访问都应遵守对应协议的返回值和异常约定;
__new__()、__getattribute__()和__del__()功能强、风险也高,只在确有需求时使用;- 先明确调用方需要的语法,再实现最小协议,不要为了“高级”而堆叠特殊方法。