Blog

__repr__ y __str__ en Python

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 !r en los campos.

  • __str__ es para lo que lee una persona. Es opcional, y cae de vuelta a __repr__.

  • Los contenedores y !r usan repr; print y las f-strings usan str.

Las dataclasses generan __repr__ por ti, que es una de las mejores razones para usar una. La parte 8 cubre esa decisión.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.