Esto va en tercer lugar, antes que las properties o la herencia, porque una clase que no puedes leer en un traceback te cuesta tiempo cada día que la tengas.
El comportamiento por defecto es inútil
class Order:
def __init__(self, id_, total):
self.id, self.total = id_, total
o = Order(17, 249.5)
print(o)
print([o, o])
Imprime algo como:
<__main__.Order object at 0x7daca41360f0>
[<__main__.Order object at 0x7daca41360f0>, <__main__.Order object at 0x7daca41360f0>]
La dirección va a ser distinta en tu computadora, y ese es justamente el problema: lo único que Python puede decirte es dónde vive el objeto en memoria, que nunca es lo que querías saber.
__repr__ lo arregla en todas partes de una vez
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])
Imprime:
Order2(id=17, total=249.5)
[Order2(id=17, total=249.5), Order2(id=17, total=249.5)]
Fíjate en la segunda línea. Los contenedores usan repr sobre lo que guardan, y por eso una lista de objetos sin __repr__ es un muro de direcciones.
La convención es devolver algo que podrías pegar de vuelta en Python para reconstruir el objeto. Order2(id=17, total=249.5) cumple. Usa !r sobre los valores que van adentro, o un campo de texto perderá sus comillas y dejará de ser válido.
str es para quien lee, repr es para ti
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}")
Imprime:
Order #17 — 249.50
Order3(id=17, total=249.5)
Order #17 — 249.50
Order3(id=17, total=249.5)
print() y las f-strings usan str. El REPL, los contenedores y !r usan repr.
Define __repr__ primero, y __str__ solo si lo necesitas
El respaldo funciona en un solo sentido. Define solo __repr__ y str() lo usa:
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))
Imprime:
Minimal(1) | Minimal(1)
Define solo __str__ y repr() te sigue dando la dirección. Así que __repr__ es el que siempre se gana su lugar; __str__ vale la pena cuando el objeto se le muestra a una persona.
Dónde se nota de verdad
En los mensajes de error y los tracebacks:
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] + '...')
Imprime:
cannot charge Order3(id=17, total=249.5)
cannot charge <__main__.Order object at 0x7dac...
El primero te dice qué pedido falló. El segundo te dice que falló un objeto y te manda de vuelta a reproducirlo. A lo largo de un año esa diferencia son horas.
Qué recordar
-
Escribe siempre
__repr__. Es la mejora de depuración más barata que existe. -
Haz que se vea como la llamada que reconstruiría el objeto, y usa
!ren los campos. -
__str__es para lo que lee una persona. Es opcional, y cae de vuelta a__repr__. -
Los contenedores y
!rusanrepr;printy las f-strings usanstr.
Las dataclasses generan __repr__ por ti, que es una de las mejores razones para usar una. La parte 8 cubre esa decisión.