Python 魔术方法有什么用?常见的 __str__、__repr__、__eq__ 怎么理解?
简化版
魔术方法是以双下划线开头和结尾的特殊方法,用来让自定义对象接入 Python 的内置语法和协议。比如 __str__ 控制面向用户的字符串展示,__repr__ 控制调试表示,__eq__ 控制 == 比较逻辑。
详细版
示例:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
使用:
p = Point(1, 2)
print(str(p)) # (1, 2)
print(repr(p)) # Point(1, 2)
print(p == Point(1, 2)) # True
常见魔术方法:
| 方法 | 作用 |
|---|---|
__init__ | 初始化对象 |
__str__ | str(obj)、print(obj) |
__repr__ | repr(obj)、调试输出 |
__eq__ | obj == other |
__len__ | len(obj) |
__iter__ | 让对象可迭代 |
__getitem__ | 支持 obj[key] |
__enter__ / __exit__ | 支持 with 上下文管理 |
完整版教学
一、魔术方法不是让你手动调用的
魔术方法通常不是这样用:
obj.__str__()
而是由 Python 的语法或内置函数自动调用:
str(obj)
print(obj)
len(obj)
obj == other
它们让自定义对象能融入 Python 语言本身。
比如实现 __len__:
class Bag:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
bag = Bag([1, 2, 3])
print(len(bag)) # 3
二、str 和 repr 的区别
__str__ 面向用户,可读即可:
def __str__(self):
return f"用户:{self.name}"
__repr__ 面向开发者和调试,最好尽量准确:
def __repr__(self):
return f"User(name={self.name!r})"
如果只实现 __repr__ 没有实现 __str__,str(obj) 通常会退回使用 __repr__。
工程里建议至少给重要领域对象实现 __repr__,排查日志时很有用。
三、eq 的正确写法
比较两个对象是否“值相等”时,可以实现 __eq__:
class User:
def __init__(self, user_id):
self.user_id = user_id
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.user_id == other.user_id
为什么返回 NotImplemented?
因为这表示当前类不知道如何和对方比较,让 Python 尝试对方类型的比较逻辑,或者最终判断为不相等。
不建议直接返回 False 处理所有陌生类型,因为可能破坏反向比较机会。
四、eq 和 hash 的关系
如果对象实现了自定义 __eq__,Python 可能会把它变成不可哈希,避免对象放进集合或作为字典 key 后行为不一致。
因为哈希对象必须满足:
如果 a == b,那么 hash(a) 必须等于 hash(b)
如果对象可变,却又能作为 dict key,修改后哈希值变化会导致字典结构出问题。
所以实现值对象时,要么让对象不可变并提供稳定 __hash__,要么不要让它可哈希。
五、运算符重载
魔术方法还能支持运算符:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
调用:
v3 = v1 + v2
背后会调用:
v1.__add__(v2)
运算符重载要符合直觉。不要让 + 做保存数据库、发请求这类副作用很强的事情。
六、协议式编程
很多魔术方法对应 Python 协议:
- 实现
__iter__,对象可迭代; - 实现
__enter__/__exit__,对象可用于with; - 实现
__getitem__,对象可下标访问; - 实现
__call__,对象可像函数一样调用。
这也是 Python 面向对象的特色:你不一定要继承某个接口,只要实现约定方法,就能接入对应语法。
七、常见误区与追问
心法:魔术方法是在实现 Python 协议,不是在堆砌特殊名字。你写
len(obj)、obj + other、for x in obj,解释器会去找对应协议方法。
- 误区:魔术方法应该直接手动调用。 通常应让语法或内置函数触发它们,例如用
len(obj)触发__len__,用str(obj)触发__str__,这样更符合协议语义。 - 追问:
__repr__和__str__面试怎么区分?__str__面向用户友好展示,__repr__面向开发者调试,理想情况下repr(obj)应尽量包含可识别对象状态的信息。 - 误区:实现
__eq__后对象还能自然作为 dict key。 如果类重写了__eq__但没有合适的__hash__,Python 会把__hash__设为None,避免可哈希契约被破坏。 - 追问:二元运算符方法返回
NotImplemented有什么用? 它告诉 Python 当前类型不处理这个操作,解释器可以尝试右操作数的反向方法,而不是立刻抛错。 - 误区:魔术方法越多,类就越 Pythonic。 只有对象确实符合某个协议时才实现对应方法;为了炫技重载不自然的运算符,会让代码更难读。
- 追问:可迭代对象最少需要实现什么? 常见方式是实现
__iter__返回迭代器;迭代器还要实现__next__,耗尽时抛StopIteration。
八、加强记忆
魔术方法是 Python 给对象接入语言语法的钩子。__str__ 给用户看,__repr__ 给开发者看,__eq__ 定义值相等。写魔术方法要符合 Python 语义预期,别为了炫技把对象行为设计得反直觉。