让你的对象像内置类型一样工作
打印一个普通对象时,Python 会显示类似 <__main__.Position object at 0x7f3a> 的内容,而 Position("A", 1) == Position("A", 1) 的结果是 False,因为默认情况下只有同一个对象才相等。特殊方法(因为双下划线也叫 dunder 方法)让你的类接入 Python 自身的语法:__repr__ 控制对象如何显示,__eq__ 控制 ==,__add__ 控制 +,__lt__ 控制 <,等等。你从不直接调用它们;当你使用 repr(x)、a == b、a + b 或 sorted(items) 时,Python 会替你调用。
class Plain:
def __init__(self, cents):
self.cents = cents
a, b = Plain(500), Plain(500)
print(a == b) # False: different objects
print(a.cents == b.cents) # True
print("object at 0x" in str(a)) # True: the default display is unhelpful__repr__:清晰地显示对象
__repr__ 返回一个面向开发者、描述对象的字符串。按惯例它看起来像构造该对象的代码:Money(1999, 'USD')。列表、调试器和错误信息都会用到它,使 bug 更易读。除非你另外定义面向用户的 __str__,否则 print(x) 和 str(x) 也使用 __repr__。它必须返回字符串而不是打印,在 f-string 中写 {self.currency!r} 可得到字符串属性带引号的形式。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
price = Money(1999)
print(price) # Money(1999, 'USD')
print([Money(5), Money(7, "EUR")]) # [Money(5, 'USD'), Money(7, 'EUR')]__eq__ 与 __hash__:相等意味着什么
__eq__(self, other) 决定 self == other 的结果。比较能确定身份的字段;当 other 不是预期类型时,返回特殊值 NotImplemented(而不是 False):Python 随后会尝试另一边,最后自己给出 False,所以 Money(1) == 5 是安全的。定义 __eq__ 会关闭哈希,除非你同时一致地定义 __hash__(例如 hash((self.cents, self.currency))),否则对象不能再作为集合成员或字典键。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented # let Python decide
return self.cents == other.cents and self.currency == other.currency
def __hash__(self):
return hash((self.cents, self.currency))
print(Money(500) == Money(500)) # True
print(Money(500) == Money(500, "EUR")) # False
print(Money(500) == 500) # False, no error
print(len({Money(1), Money(1)})) # 1: usable in sets now运算符与排序
__add__(self, other) 让你的类支持 +。应返回新对象而不是修改 self,并拒绝无意义的操作:美元加欧元应引发 ValueError,加一个裸数字应返回 NotImplemented。__lt__(self, other) 提供 <,仅凭它,sorted()、min() 和 max() 就能对你的对象排序,因为它们只需要问“a 比 b 小吗?”。也可以补上 __le__、__gt__、__ge__,或让 functools.total_ordering 自动推导。
class Money:
def __init__(self, cents, currency="USD"):
self.cents = cents
self.currency = currency
def __repr__(self):
return f"Money({self.cents}, {self.currency!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError("cannot add different currencies")
return Money(self.cents + other.cents, self.currency)
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.cents < other.cents
total = Money(150) + Money(275)
print(total) # Money(425, 'USD')
print(sorted([Money(30), Money(10), Money(20)])) # [Money(10, 'USD'), Money(20, 'USD'), Money(30, 'USD')]
print(sum([Money(1), Money(2)], Money(0))) # Money(3, 'USD')__repr__ 应显示 __eq__ 所比较的全部内容。不要让 + 带来令人意外的副作用:它应返回新值,并像数字那样保持两个操作数不变。实验的测试会检查这些要点:原对象不变、拒绝不同币种、无关类型返回 NotImplemented。__repr__返回字符串(如Money(1999, 'USD')),不打印。__eq__比较字段,对外来类型返回NotImplemented;加上__hash__以保留哈希能力。__add__返回新对象;__lt__足以支持sorted、min和max。
下一节:数据类(dataclass),它会替你写好 __init__、__repr__ 和 __eq__。
实验:评分内容
- 用
__repr__清晰地显示 Money 对象25 分 - 用
__eq__定义两个金额何时相等25 分 - 用
__add__把两个金额相加25 分 - 用
__lt__让金额可以排序25 分