044、魔术方法实战(一):__str__、__repr__、__eq__、__hash__

📅 2026/6/26 0:20:56
044、魔术方法实战(一):__str__、__repr__、__eq__、__hash__
044、魔术方法实战一str、repr、eq、hash一个让我熬夜到凌晨三点的Bug去年接手一个老项目日志系统里打印用户对象时全是User object at 0x7f8c2a1b3d30这种鬼东西。排查问题时我盯着控制台看了半小时愣是分不清哪个是哪个。更崩溃的是把用户对象塞进集合set时明明两个用户ID相同却当成不同对象存了两份——后来发现是__hash__和__eq__没配合好。那次之后我彻底明白了魔术方法不是花架子是保命符。今天就把这几个最常用的魔术方法掰开揉碎讲清楚。str给用户看的“名片”先看一个反面教材classUser:def__init__(self,name,age):self.namename self.ageage userUser(张三,25)print(user)# 输出__main__.User object at 0x...这玩意儿在调试时毫无意义。正确的做法是定义__str__classUser:def__init__(self,name,age):self.namename self.ageagedef__str__(self):returnf用户{self.name}年龄{self.age}userUser(张三,25)print(user)# 输出用户张三年龄25这里踩过坑__str__必须返回字符串别手滑返回了数字或列表。Python 不会帮你做类型转换直接抛TypeError。repr给开发者看的“身份证”__repr__和__str__容易搞混。记住一句话__str__是给人看的__repr__是给解释器看的。classUser:def__init__(self,name,age):self.namename self.ageagedef__repr__(self):returnfUser({self.name},{self.age})userUser(张三,25)print(repr(user))# 输出User(张三, 25)别这样写把__repr__返回成和__str__一样的内容。__repr__的理想状态是把这个字符串扔回eval()能重建对象。虽然不强制但这是 Python 社区的约定俗成。实战中如果只实现了__repr__没实现__str__Python 会拿__repr__的结果当__str__用。所以偷懒的话只写__repr__也行——但别这么干两个都写才是专业做法。eq判断“相等”的潜规则默认情况下两个对象比较的是内存地址classUser:def__init__(self,user_id,name):self.user_iduser_id self.namename u1User(1,张三)u2User(1,张三)print(u1u2)# False因为内存地址不同业务逻辑里我们通常认为 user_id 相同就是同一个人classUser:def__init__(self,user_id,name):self.user_iduser_id self.namenamedef__eq__(self,other):ifnotisinstance(other,User):returnFalse# 别这样写直接抛异常应该优雅返回 Falsereturnself.user_idother.user_id u1User(1,张三)u2User(1,李四)# 名字不同但ID相同print(u1u2)# True这里踩过坑__eq__返回的不一定是布尔值。如果你返回了None或0Python 会把它当布尔值处理但逻辑可能出错。务必返回True或False。hash和__eq__的“夫妻关系”这是最容易被忽视的坑。Python 规定如果两个对象相等__eq__返回 True它们的哈希值必须相等。反过来不成立——哈希值相等不代表对象相等哈希碰撞。classUser:def__init__(self,user_id,name):self.user_iduser_id self.namenamedef__eq__(self,other):ifnotisinstance(other,User):returnFalsereturnself.user_idother.user_iddef__hash__(self):returnhash(self.user_id)# 和__eq__用同一个字段u1User(1,张三)u2User(1,李四)# 现在可以正常放进集合了user_set{u1,u2}print(len(user_set))# 1因为u1和u2相等# 也可以作为字典键user_dict{u1:数据1}print(user_dict[u2])# 输出数据1因为u2的哈希值和u1相同别这样写__hash__返回固定值比如return 1。虽然符合规则但会导致哈希表退化成链表性能暴跌。另一个坑如果定义了__eq__没定义__hash__Python 会自动把__hash__设为None。这意味着对象变成不可哈希的不能放进集合或作为字典键。所以要么两个都定义要么都不定义。实战一个完整的用户类把上面这些整合起来写一个生产级别的用户类classUser:def__init__(self,user_id,name,email):self.user_iduser_id self.namename self.emailemaildef__str__(self):# 给用户看的信息简洁明了returnf{self.name}({self.email})def__repr__(self):# 给开发者看能重建对象returnfUser(user_id{self.user_id}, name{self.name}, email{self.email})def__eq__(self,other):ifnotisinstance(other,User):returnNotImplemented# 这里用NotImplemented而不是False让Python尝试反向比较returnself.user_idother.user_iddef__hash__(self):returnhash(self.user_id)# 测试u1User(1,张三,zhangsanexample.com)u2User(1,张三,zhangsanexample.com)u3User(2,李四,lisiexample.com)print(u1)# 张三(zhangsanexample.com)print(repr(u1))# User(user_id1, name张三, emailzhangsanexample.com)print(u1u2)# Trueprint(u1u3)# False# 集合去重users{u1,u2,u3}print(len(users))# 2因为u1和u2被视为同一个个人经验性建议调试时优先写__repr__。__str__可以等上线前再补但__repr__在开发阶段能救你无数次。我习惯在写完类结构后立刻补上__repr__哪怕只返回类名和ID。__eq__和__hash__必须用同一组字段。这是铁律。如果__eq__比较了三个字段__hash__也得用这三个字段算哈希。否则会出现“两个对象相等但哈希不同”的诡异情况集合和字典会直接崩。不要轻易让对象可变。如果对象在放进集合后修改了参与__hash__的字段哈希值会变导致对象在集合里“丢失”。我见过一个线上事故用户对象放进集合后改了ID结果再也查不到了。解决方案要么用不可变对象比如namedtuple要么把参与哈希的字段设为只读。NotImplemented比False更优雅。在__eq__里遇到类型不匹配时返回NotImplemented而不是False。这样 Python 会尝试调用对方的__eq__给子类或第三方类留出扩展空间。别滥用__hash__。如果类不需要放进集合或作为字典键就别定义__hash__。保持简单避免不必要的复杂度。最后说句实在话这些魔术方法写起来不费事但漏掉一个可能让你多花三小时排查。我的习惯是——写完类定义后立刻问自己三个问题这个类需要打印吗需要比较吗需要去重吗然后对应补上__str__/__repr__、__eq__、__hash__。养成肌肉记忆比什么都强。