Aprende leyendo en orden

Métodos especiales — Enseñar a tu clase cómo se comportan + y print

Aprende los métodos especiales de Python (métodos dunder). Personaliza + con __add__, controla la salida de print con __str__ y redefine == con __eq__ — todo con diagramas.

La última vez ordenamos los métodos de instancia, de clase y estáticos. Ahora cubriremos un tipo distinto — los métodos especiales (también conocidos como métodos dunder).

¿Qué son los métodos especiales? — "Métodos dunder"

Métodos como __init__ y __str__ — nombres rodeados de dos guiones bajos a cada lado — se llaman métodos especiales. Como están entre double underscores (dobles guiones bajos), también se les conoce como métodos dunder.

Lo clave de los métodos especiales es que no los llamas directamente. Python los llama automáticamente cuando realizas ciertas operaciones. Escribe v1 + v2 y Python llama a v1.__add__(v2) por debajo. Escribe print(p) y se llama a p.__str__(). Escribe a == b y se ejecuta a.__eq__(b).

Operadores y sus métodos especiales
Lo queescribesPythonconvierteMétodollamadov1 + v2__add__print(p)__str__a == b__eq__
Los operadores y las funciones integradas de Python llaman a los métodos dunder correspondientes entre bastidores. Defínelos en tu clase y tu propio tipo podrá usar +, == y compañía.

__add__ — Definir el operador +

Toma + como ejemplo evidente. Supón que tienes una clase Money para importes en yenes y quieres que Money(300) + Money(500) te dé Money(800). Intenta sumarlos sin más y Python lanzará un TypeError quejándose de que no sabe cómo sumar Money con Money.

La forma de enseñar a Python a sumarlos es el método __add__. Define def __add__(self, other): y devuelve una nueva instancia construida a partir de self (lado izquierdo) y other (lado derecho). Ahora + funciona en tu clase.

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):                 # se llama al escribir +
        return Money(self.amount + other.amount)

wallet  = Money(300)
payment = Money(500)
total   = wallet + payment                    # en realidad wallet.__add__(payment)
print(total.amount)                           # 800
Lo que hace v1 + v2 por debajo
wallet+ paymentwallet.__add__(payment)self =walletother =paymentreturn Money(self.amount + other.amount)devuelveMoney(800)traduce
Escribe + y Python llama a self.__add__(other). self es el operando izquierdo, other el derecho. La nueva instancia que devuelvas se convierte en el resultado de +.

Implementa __add__ en Money para que + sume dos saldos.

① Define class Money: con __init__(self, amount) que asigne self.amount = amount.

② Define __add__(self, other) que devuelva Money(self.amount + other.amount).

③ Construye wallet = Money(300) y payment = Money(500), luego haz print de total.amount donde total = wallet + payment.

(Cuando se ejecute correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

__str__ y __repr__ — Dos tipos de representación de cadena

Los siguientes: los métodos especiales que se llaman cuando una instancia se convierte en cadena. Hay dos, con objetivos distintos.

- __str__ — llamado por print(p) y str(p). Una cadena legible para el usuario.

- __repr__ — llamado en el REPL o para depuración. Una cadena estilo código, detallada.

Sin sobrescrituras, print(user) muestra algo como <__main__.User object at 0x...> — bastante inútil. Define __str__ para una visualización limpia, y define también __repr__ para que la depuración te muestre el tipo y el contenido de un vistazo.

class User:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def __str__(self):                       # para print() / str()
        return f"{self.name} ({self.age} años)"

    def __repr__(self):                      # para depuración
        return f"User(name={self.name!r}, age={self.age})"

u = User("Ana", 30)
print(u)             # Ana (30 años)               <- __str__
print(repr(u))       # User(name='Ana', age=30)    <- __repr__
Cuándo usar __str__ vs __repr__
__str__ (para el usuario)
  • Llamado por print(u) y str(u)
  • Objetivo: una cadena que lean los usuarios finales
  • Ejemplo: Ana (30 años)
__repr__ (para el desarrollador)
  • Llamado por repr(u) y por el REPL interactivo
  • Aquí cae como respaldo cuando falta __str__ durante print
  • Objetivo: una cadena de depuración con tipo y contenido
  • Ejemplo: User(name='Ana', age=30)
Definir ambos es lo ideal, pero si solo defines uno, que sea __repr__ — facilita mucho la depuración.

Implementa __str__ y __repr__ en User y compara lo que muestran print y repr.

① Define class User: con __init__(self, name, age) que asigne self.name / self.age.

② Define __str__(self) que devuelva f"{self.name} ({self.age} años)".

③ Define __repr__(self) que devuelva f"User(name={self.name!r}, age={self.age})". El !r después de self.name significa incrustar el resultado de llamar a repr() sobre el valor — las cadenas salen con comillas simples ('Ana') en vez de texto pelado (Ana).

④ Construye u = User("Ana", 30), luego ejecuta tanto print(u) como print(repr(u)) para ver la diferencia.

Editor Python

Ejecutar el código para ver el resultado

__eq__ — Definir ==

Ahora la igualdad. Cuando escribes a == b, Python llama internamente a a.__eq__(b). Si tu clase no define __eq__, el comportamiento por defecto es "¿son el mismo objeto en memoria?" (una comparación de id).

Digamos que tienes una clase Coupon y quieres que dos cupones con el mismo código cuenten como el mismo cupón — incluso si los valores de descuento difieren. Esa es una igualdad "a nivel de negocio", y __eq__ es donde lo deletreas.

Cómo __eq__ cambia el comportamiento de ==
c1 == c2sin__eq__comp. de id(dir. memoria)con__eq__self.code== other.codepor defectodefinido
Sin __eq__, == es una comprobación de id — el mismo contenido pero instancias separadas siguen dando False. Con __eq__ puedes comparar por contenido (code).
class Coupon:
    def __init__(self, code, discount):
        self.code     = code
        self.discount = discount

    def __eq__(self, other):
        return self.code == other.code        # mismo code = mismo cupón

c1 = Coupon("SPRING10", 0.10)
c2 = Coupon("SPRING10", 0.20)               # descuento distinto, mismo code
c3 = Coupon("SUMMER15", 0.15)

print(c1 == c2)   # True  (los codes coinciden)
print(c1 == c3)   # False (los codes difieren)

¿Qué pasa sin __eq__?

Si no defines __eq__, c1 == c2 comprueba si son el mismo objeto en memoria (literalmente apuntan a la misma caja). Aunque cada atributo coincida perfectamente, dos instancias construidas por separado viven en direcciones de memoria distintas y el resultado es False. Siempre que quieras "igual por contenido", define tú mismo __eq__.

Implementa __eq__ en Coupon para que dos cupones sean iguales cuando coincida su code.

① Define class Coupon: con __init__(self, code, discount) que asigne self.code y self.discount.

② Define __eq__(self, other) que devuelva self.code == other.code.

③ Construye tres cupones — Coupon("SPRING10", 0.10), Coupon("SPRING10", 0.20) y Coupon("SUMMER15", 0.15) — y haz print de dos comparaciones ==.

Editor Python

Ejecutar el código para ver el resultado

Otros métodos especiales comunes — __call__ / __len__ / __bool__

Más allá de los cuatro que hemos cubierto, varios otros métodos especiales aparecen a menudo. Tres que vale la pena conocer ya:

- __call__ — hace que una instancia sea invocable como una función (sintaxis obj(...))

- __len__ — define lo que devuelve len(obj)

- __bool__ — define cómo se evalúa la instancia como valor de verdad en if obj: o bool(obj)

Cada uno enseña una operación integrada — llamada de función, len(), la comprobación de veracidad de if — a tu clase personalizada. Un Logger que acumula entradas de log es un gran ejemplo para poner los tres en una sola clase.

class Logger:
    def __init__(self, name):
        self.name = name
        self.log  = []

    def __call__(self, message):                    # logger("...") funciona
        self.log.append(message)
        return f"[{self.name}] {message}"

    def __len__(self):                               # para len(logger)
        return len(self.log)

    def __bool__(self):                              # para if logger:
        return len(self.log) > 0

app = Logger("app")
print(app("App iniciada"))    # [app] App iniciada
print(app("Login correcto"))   # [app] Login correcto
print(len(app))               # 2
if app:
    print("Hay logs")    # Hay logs
Enseñar a Logger a hablar la sintaxis integrada
app('msg')app.__call__('msg')len(app)app.__len__()if app:app.__bool__()se vuelvese vuelvese vuelve
Sintaxis familiar — logger(...), len(logger), if logger: — se enseña a tu clase personalizada mediante tres dunders correspondientes.

¿Y si no defines __bool__?

Cuando falta __bool__, Python cae en __len__. Una longitud de 0 se vuelve False; cualquier otra cosa se vuelve True. Si faltan ambos, las instancias son siempre verdaderas, sin importar su estado. Esa es la misma regla que hace que las listas y cadenas vacías se evalúen como False.

Implementa los tres métodos especiales en Logger para que añadir mensajes, contarlos y comprobar la vacuidad funcionen todos con la sintaxis integrada.

① Define class Logger: con __init__(self, name) que ponga self.name = name y self.log = [].

② Define __call__(self, message): añade message a self.log, luego devuelve f"[{self.name}] {message}".

③ Define __len__(self) que devuelva len(self.log).

④ Define __bool__(self) que devuelva len(self.log) > 0.

⑤ Construye error_log = Logger("error"), luego ejecuta en orden: print(bool(error_log))error_log("Error de BD")print(len(error_log))print(bool(error_log)).

Editor Python

Ejecutar el código para ver el resultado

Hay muchos más — __hash__ (para usar como clave de set/dict), __lt__ / __gt__ (las comparaciones < / >), __getitem__ (la sintaxis obj[key]), __iter__ (iteración for x in obj:) y otros. No necesitas memorizarlos todos. Solo guarda este mapa mental: "si quieres enseñar una operación integrada a tu clase, probablemente haya un dunder correspondiente para ello." Luego búscalo cuando lo necesites.

Método especialSintaxisCuándo se llama
__init__Money(300)Al crear la instancia
__add__a + bOperador +
__str__print(p) / str(p)Cadena para el usuario
__repr__repr(p) / pantalla del REPLCadena para el desarrollador
__eq__a == bComparación de igualdad
__call__obj(...)Llamada estilo función
__len__len(obj)Longitud
__bool__if obj: / bool(obj)Veracidad
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Cuando se ejecuta result = a + b, ¿qué método llama Python entre bastidores?

Pregunta 2¿Qué descripción de __str__ y __repr__ es más exacta?

Pregunta 3Si una clase no define __eq__, ¿qué compara a == b? (Ambas son instancias de la misma clase.)