没有 __repr__ 的类,从写出来那天起就在浪费你的时间。repr 给你自己看,str 给读者看,而两者中只有一个有像样的回退。
这篇排在第三,放在属性和继承之前,因为在回溯信息(traceback)里看不懂的类,只要还留在代码里,每天都在耗你的时间。
默认输出毫无用处
class Order:
def __init__(self, id_, total):
self.id, self.total = id_, total
o = Order(17, 249.5)
print(o)
print([o, o])
输出类似这样:
<__main__.Order object at 0x7daca41360f0>
[<__main__.Order object at 0x7daca41360f0>, <__main__.Order object at 0x7daca41360f0>]
在你的机器上地址会不一样,而问题恰恰在这里:Python 唯一能告诉你的,是对象在内存里的位置,而这从来不是你想知道的。
一个 __repr__ 处处生效
class Order2:
def __init__(self, id_, total):
self.id, self.total = id_, total
def __repr__(self):
return f"Order2(id={self.id!r}, total={self.total!r})"
o2 = Order2(17, 249.5)
print(o2)
print([o2, o2])
输出:
Order2(id=17, total=249.5)
[Order2(id=17, total=249.5), Order2(id=17, total=249.5)]
注意第二行。容器对里面的元素用的是 repr,所以一个装着没有 __repr__ 的对象的列表,打印出来就是一堆地址。
约定是返回一段能粘回 Python 里重建这个对象的文本。Order2(id=17, total=249.5) 就符合。里面的值要加 !r,否则字符串字段会丢掉引号,结果就不再是合法的代码。
str 给读者看,repr 给你看
class Order3:
def __init__(self, id_, total):
self.id, self.total = id_, total
def __repr__(self):
return f"Order3(id={self.id!r}, total={self.total!r})"
def __str__(self):
return f"Order #{self.id} — {self.total:,.2f}"
o3 = Order3(17, 249.5)
print(str(o3))
print(repr(o3))
print(f"{o3}")
print(f"{o3!r}")
输出:
Order #17 — 249.50
Order3(id=17, total=249.5)
Order #17 — 249.50
Order3(id=17, total=249.5)
print() 和 f-string 用 str。REPL、容器和 !r 用 repr。
先写 __repr__,需要时再写 __str__
回退只有一个方向。只定义 __repr__,str() 会用它:
class Minimal:
def __init__(self, v): self.v = v
def __repr__(self): return f"Minimal({self.v!r})"
m = Minimal(1)
print(str(m), '|', repr(m))
输出:
Minimal(1) | Minimal(1)
只定义 __str__ 的话,repr() 给你的还是地址。所以 __repr__ 永远值得写;__str__ 只在对象要展示给人看时才值得加。
真正见效的地方
错误信息和回溯:
def charge(order):
raise ValueError(f"cannot charge {order!r}")
try:
charge(o3)
except ValueError as err:
print(err)
try:
charge(o)
except ValueError as err:
print(str(err)[:46] + '...')
输出:
cannot charge Order3(id=17, total=249.5)
cannot charge <__main__.Order object at 0x7dac...
第一行告诉你是哪个订单出了问题。第二行只告诉你某个对象出了问题,你还得回头去复现。一年下来,这点差别就是好几个小时。
要点
- 永远写
__repr__。这是成本最低的调试改进。 - 让它看起来像能重建对象的那次调用,字段上用
!r。 __str__用于给人读的输出。它是可选的,没定义时会回退到__repr__。- 容器和
!r用repr;print和 f-string 用str。
数据类(dataclass)会自动生成 __repr__,这是选用它的好理由之一。第 8 篇会讲怎么选。