直接重写__eq__容易出错,因手动递归需处理嵌套对象、none、循环引用、类型不一致及可变性等问题;用vars()+递归虽适用多数数据类,但需显式处理容器、避免污染状态,且深度比较可能影响哈希一致性。

为什么直接重写 __eq__ 容易出错
直接在 __eq__ 里用 == 比较所有属性,看似简单,但遇到嵌套对象、None、循环引用或自定义类没实现 __eq__ 时,会立刻抛 RecursionError 或返回错误结果。Python 默认的 == 对实例只比较身份(is),不会自动递归;而手动递归又得自己处理类型、可变性、边界条件。
用 vars() + 递归检查所有实例属性
适用于大多数数据类场景:属性全是实例变量(非 property、非描述符)、无循环引用、不依赖私有字段逻辑。核心是提取当前对象所有可枚举属性,再逐个比对值——包括嵌套对象。
-
vars(self)返回对象的__dict__,只含实例属性,不含类变量或方法 - 对每个属性值,先判断是否为自定义对象(
hasattr(v, '__dict__')且非内置类型),再递归调用__eq__ - 必须显式处理
None、list、dict、tuple等容器,因为它们的==已有语义,无需再拆 - 避免无限递归:加一个
seen集合记录已访问的id(obj),遇到重复引用立即返回True(相同引用视为相等)
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, self.__class__):
return False
if hasattr(other, '_comparing') and getattr(other, '_comparing', False):
return self is other # 循环引用兜底
try:
self._comparing = True
other._comparing = True
self_attrs = vars(self)
other_attrs = vars(other)
if set(self_attrs.keys()) != set(other_attrs.keys()):
return False
for k in self_attrs:
v1, v2 = self_attrs[k], other_attrs[k]
if type(v1) != type(v2):
return False
if isinstance(v1, (list, tuple, dict, set)):
if v1 != v2: # 容器用默认 == 即可
return False
elif hasattr(v1, '__dict__') and not isinstance(v1, (str, bytes, int, float, bool)):
if not (v1 == v2): # 递归进子对象
return False
else:
if v1 != v2:
return False
return True
finally:
self._comparing = False
other._comparing = False
更稳妥的做法:用 dataclasses.asdict() 或 pydantic.BaseModel
如果你的类本质是数据容器,硬写 __eq__ 是重复造轮子。Python 3.7+ 的 @dataclass 默认开启 eq=True,但它是浅比较;要深度比较,可借助 asdict() 转成嵌套字典再比:
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
-
asdict()会递归展开所有 dataclass 字段,跳过方法和类变量,天然支持嵌套 - 缺点:不处理非 dataclass 的自定义对象(比如你有个字段是手写的
User类,它没用@dataclass),这时仍需手动实现其__eq__ - 若已用
pydantic.BaseModel,直接用model_dump()(v2)或dict()(v1),它默认深度序列化,且自动忽略ComputedField和私有属性
示例:return asdict(self) == asdict(other) —— 简洁,但要注意性能:每次比较都新建完整字典树。
最容易被忽略的坑:不可哈希对象与缓存失效
深度比较本身不改变对象,但如果你在 __eq__ 里用了 seen 集合或临时标记(如上面的 _comparing),就可能污染对象状态。尤其当对象被放进 set 或作为 dict 键时,__hash__ 可能依赖这些字段,导致行为异常。
- 永远不要在
__eq__中修改对象属性(哪怕临时) - 用线程局部存储(
threading.local())或函数参数传入seen集合,而不是存在实例上 - 如果类需要同时支持
==和哈希,__hash__必须只基于不可变字段,且不能依赖深度结构(否则哈希值随嵌套内容变化,破坏哈希表前提)
真正复杂的嵌套结构比较,往往不是“怎么写 __eq__”,而是该不该让它支持比较——有时显式调用 deep_equals(a, b) 函数,比侵入类定义更清晰、更可控。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










