Desde 05-01 arrastramos una deuda: print(odisea) muestra <__main__.Libro object at 0x...>. Y hay más carencias silenciosas: dos objetos Libro con el mismo título no son iguales para ==, sorted() no sabe ordenarlos sin key=, y len() los rechaza. Los tipos integrados hacen todo eso con naturalidad porque implementan métodos mágicos (o dunder methods, por double underscore): métodos con nombre __asi__ que Python invoca automáticamente cuando usas operadores, print, len, in, sorted... En esta lección tus clases aprenderán ese idioma, con un vehículo nuevo del hilo de Papyrus: la clase Carrito, la cesta de la compra de la tienda de Ana.
Contenido
- Qué es un método mágico (y uno que ya conoces)
__str__vs__repr__: saldar elprintfeo__eq__: cuándo dos libros son el mismo libro__lt__: ordenar consorted()sinkey=- La clase
Carritode Papyrus __len__,__contains__y__add__en el carrito- Tabla de los dunder más usados
- No abusar de la magia
- Errores comunes y consejos
- Ejercicios con soluciones
Qué es un método mágico (y uno que ya conoces)
Un método mágico es un método con doble guion bajo al principio y al final que tú defines pero no llamas: lo llama Python al usar cierta sintaxis. Ya conoces uno: __init__, que se ejecuta solo al escribir Libro(...). La tabla mental es siempre la misma:
| Tú escribes... | Python ejecuta... |
|---|---|
Libro("Hamlet", 9.95) |
Libro.__init__(objeto, "Hamlet", 9.95) |
print(libro) / str(libro) |
libro.__str__() |
libro_a == libro_b |
libro_a.__eq__(libro_b) |
libro_a < libro_b |
libro_a.__lt__(libro_b) |
len(carrito) |
carrito.__len__() |
libro in carrito |
carrito.__contains__(libro) |
carrito_a + carrito_b |
carrito_a.__add__(carrito_b) |
Aquí está el mecanismo que 05-03 dejó anunciado: len() es polimórfica porque cada tipo trae su __len__. Los operadores de Python son, en el fondo, mensajes polimórficos.
__str__ vs __repr__: saldar el print feo
Python distingue dos representaciones en texto de un objeto:
__str__: para personas. La usaprint(),str()y las f-strings. Debe ser legible y agradable.__repr__: para programadores. La usa la consola interactiva, los mensajes de depuración y las colecciones (print(lista_de_libros)usa el__repr__de cada elemento). La convención de oro: que parezca el código necesario para recrear el objeto.
class Libro:
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def __init__(self, titulo, precio, stock=0):
self.titulo = titulo
self.precio = precio
self.stock = stock
def precio_final(self, socio=False):
descuento = Libro.DESCUENTO_SOCIO if socio else 0
return round(self.precio * (1 - descuento) * (1 + Libro.IVA_LIBROS), 2)
def __str__(self):
return f"{self.titulo} ({self.precio:.2f} €, stock: {self.stock})"
def __repr__(self):
return f"Libro({self.titulo!r}, {self.precio}, {self.stock})"odisea = Libro("La Odisea", 12.50, 4)
print(odisea) # La Odisea (12.50 €, stock: 4) ← __str__
print(f"Nuevo: {odisea}") # Nuevo: La Odisea (12.50 €, stock: 4) ← __str__
print([odisea]) # [Libro('La Odisea', 12.5, 4)] ← __repr__
odisea # Libro('La Odisea', 12.5, 4) (en la consola interactiva)Gancho de 05-01 saldado. Dos apuntes: el !r dentro de la f-string pide el repr del valor (por eso el título sale con comillas), y si solo defines __repr__, print() lo usará como respaldo — por eso, si solo vas a escribir uno, que sea __repr__.
__eq__: cuándo dos libros son el mismo libro
Sin __eq__, == compara identidad (¿son el mismo objeto en memoria?), no contenido:
a = Libro("Hamlet", 9.95, 6)
b = Libro("Hamlet", 9.95, 6)
print(a == b) # False (!) — objetos distintos, aunque idénticosPara Papyrus decidimos una regla de negocio: dos libros son el mismo si su título normalizado coincide — la misma normalización strip().casefold() que buscar_libro() usa desde 04-05:
def __eq__(self, otro):
if not isinstance(otro, Libro):
return NotImplemented
return self.titulo.strip().casefold() == otro.titulo.strip().casefold()NotImplemented(sin lanzar error) le dice a Python "yo no sé compararme con eso": asíodisea == 42devuelveFalselimpiamente en lugar de romperse, y Python puede darle la oportunidad al otro operando. Es uno de los usos legítimos deisinstance(05-03): comprobar antes de comparar, no decidir comportamiento.- Con
__eq__definido,!=funciona solo (Python lo deriva).
Un efecto colateral que debes conocer: al definir __eq__, Python desactiva el hash por defecto y el objeto deja de poder guardarse en sets o como clave de diccionario (04-04). Es deliberado: igualdad y hash deben ser coherentes. La solución (definir __hash__) queda anotada en la tabla final; la dataclass de la próxima lección lo gestionará por nosotros.
__lt__: ordenar con sorted() sin key=
sorted() solo necesita saber si un elemento es "menor que" otro: exactamente lo que expresa __lt__ (less than). Definimos el orden natural de los libros de Papyrus como el alfabético por título normalizado:
def __lt__(self, otro):
if not isinstance(otro, Libro):
return NotImplemented
return self.titulo.casefold() < otro.titulo.casefold()estanteria = [Libro("Hamlet", 9.95, 6), Libro("Fausto", 21.00, 0),
Libro("La Odisea", 12.50, 4), Libro("El Quijote", 15.90, 8)]
for libro in sorted(estanteria): # ¡sin key=!
print(libro)El Quijote (15.90 €, stock: 8) Fausto (21.00 €, stock: 0) Hamlet (9.95 €, stock: 6) La Odisea (12.50 €, stock: 4)
min(), max() y list.sort() también funcionan ya sin key=. Criterio de diseño: define __lt__ solo si tu clase tiene un orden natural evidente. Si unas veces ordenas por precio y otras por stock, no hay orden natural: sigue usando key= con lambdas (03-03), que para eso está.
La clase Carrito de Papyrus
Los clientes de Ana llenan cestas, y una cesta pide a gritos comportarse como colección: tener longitud, poder preguntarle "¿está este libro?", sumarse con otra. Es composición (05-02): el carrito tiene libros.
class Carrito:
"""La cesta de la compra de un cliente de Papyrus."""
def __init__(self, cliente, libros=None):
self.cliente = cliente
self._libros = list(libros) if libros else [] # copia defensiva (04-01)
def agregar(self, libro):
self._libros.append(libro)
def total(self, socio=False):
return round(sum(libro.precio_final(socio) for libro in self._libros), 2)
def __len__(self):
return len(self._libros)
def __contains__(self, buscado):
return any(libro == buscado for libro in self._libros)
def __add__(self, otro):
if not isinstance(otro, Carrito):
return NotImplemented
nombre = f"{self.cliente} y {otro.cliente}"
return Carrito(nombre, self._libros + otro._libros) # ¡carrito NUEVO!
def __str__(self):
return f"Carrito de {self.cliente}: {len(self)} artículos, {self.total():.2f} €"
def __repr__(self):
return f"Carrito({self.cliente!r}, {self._libros!r})"__len__, __contains__ y __add__ en el carrito
odisea = Libro("La Odisea", 12.50, 4)
hamlet = Libro("Hamlet", 9.95, 6)
quijote = Libro("El Quijote", 15.90, 8)
carrito_julia = Carrito("Júlia", [odisea, hamlet])
carrito_omar = Carrito("Omar", [quijote])
print(len(carrito_julia)) # 2 → __len__
print(hamlet in carrito_julia) # True → __contains__
print(quijote in carrito_julia) # False
pedido_conjunto = carrito_julia + carrito_omar # __add__
print(pedido_conjunto) # Carrito de Júlia y Omar: 3 artículos, 39.89 €
print(len(carrito_julia)) # 2 → los originales quedan intactosTres decisiones de diseño que conviene subrayar:
__contains__reutiliza__eq__: comoLibrocompara por título normalizado,Libro(" hamlet ", 0) in carrito_juliadaTrue. Los dunder se apoyan unos en otros — coherencia gratis.__add__devuelve un carrito NUEVO y no modifica los operandos, igual que[1, 2] + [3]crea una lista nueva. Un+que mutara sus operandos sería una trampa para quien lo use.__len__regala truthiness (02-01): un carrito con__len__a 0 es falso en unif, así queif carrito:significa "si tiene algo" — exactamente como listas y diccionarios.- ¿Y sumar carritos tenía sentido? Aquí sí (Júlia y Omar comparten pedido para ahorrar envío): el
+tiene un significado evidente en el dominio. Esa es la vara de medir, como verás enseguida.
Tabla de los dunder más usados
| Método | Lo activa | Debe devolver | En Papyrus |
|---|---|---|---|
__init__ |
Clase(...) |
None |
Inicializar libro/carrito |
__str__ |
print(), str(), f-strings |
str legible |
"La Odisea (12.50 €, stock: 4)" |
__repr__ |
Consola, depuración, colecciones | str recreable |
"Libro('La Odisea', 12.5, 4)" |
__eq__ |
==, !=, in (vía contains por defecto) |
bool o NotImplemented |
Mismo título normalizado |
__lt__ |
<, sorted(), min(), max() |
bool o NotImplemented |
Orden alfabético |
__hash__ |
sets, claves de dict | int coherente con __eq__ |
Necesario si defines __eq__ y quieres sets |
__len__ |
len(), truthiness |
int ≥ 0 |
Artículos del carrito |
__contains__ |
x in obj |
bool |
¿Libro en el carrito? |
__add__ |
+ |
Objeto nuevo o NotImplemented |
Fusionar carritos |
__getitem__ |
obj[clave], slicing |
El elemento | (No lo necesitamos... aún) |
__iter__ |
for x in obj |
Un iterador | Llegará con los generadores (M8) |
No abusar de la magia
Los métodos mágicos son azúcar potentísimo, y por eso mismo peligroso. La regla: implementa un dunder solo si la operación tiene un significado obvio e indiscutible en tu dominio.
- ¿
carrito_a + carrito_b? Claro: juntar pedidos. ✔ - ¿
libro_a + libro_b? ¿Qué sería eso: un tomo doble, sumar precios, concatenar títulos? Ambiguo → método explícito con nombre (pack_con(otro)o similar). ✘ - ¿
carrito - libro? Discutible;carrito.quitar(libro)se entiende sin abrir la documentación. ✘ - ¿
libro * 3? No:libro.precio_final() * 3o un métodosubtotal(unidades=3). ✘
Quien lee a + b no puede consultar el manual de cada + del programa. Si dudas entre un operador y un método con nombre, gana el nombre. Los dunder de representación (__str__, __repr__) son la excepción: defínelos casi siempre, nunca estorban.
Errores Comunes y Consejos
- Llamar a los dunder directamente (
libro.__str__(),carrito.__len__()): funciona, pero es estilo pobre. Escribestr(libro)ylen(carrito); los dunder son para definir, no para llamar. - Devolver algo que no es cadena en
__str__/__repr__:TypeError: __str__ returned non-string. Devuelve siemprestr, no hagasprintdentro. - Lanzar error en
__eq__ante tipos ajenos en lugar de devolverNotImplemented: rompe cosas tan básicas comolibro in lista_mixta. Compara conisinstancey delega conNotImplemented. - Definir
__eq__y olvidar el efecto sobre__hash__: tus objetos dejarán de entrar en sets y dicts (TypeError: unhashable type). Si los necesitas ahí, define__hash__sobre los mismos datos que la igualdad:def __hash__(self): return hash(self.titulo.strip().casefold()). - Mutar
selfdentro de__add__:+debe crear un objeto nuevo. Si quieres mutar, el operador es+=(__iadd__) o, mejor al empezar, un métodoagregar(). __str__que oculta información en depuración: para inspeccionar usarepr()(o!ren f-strings), que para eso existe la pareja.- Consejo: el orden de escritura sano en una clase nueva es:
__init__→__repr__→__str__→ y solo después, los operadores que el dominio pida a gritos.
Ejercicios
Ejercicio 1: Socio presentable y comparable
Da a la clase Socio (nombre, codigo) un __repr__ canónico, un __str__ tipo "Luis (LUIS-001)" y un __eq__ que considere iguales dos socios con el mismo codigo (da igual el nombre). Comprueba que Socio("Luis", "LUIS-001") == Socio("Luís", "LUIS-001") es True.
Ejercicio 2: la biblioteca medible y ordenada
Crea la clase Biblioteca que contenga objetos Libro (composición) con: __len__ (número de ejemplares totales, sumando stocks — ojo, no número de títulos), __contains__ (por igualdad de libros) y __str__ ("Biblioteca Papyrus: 18 ejemplares de 4 títulos"). Pruébala con el catálogo canónico (stocks 4, 6, 8, 0).
Ejercicio 3: sumar una lista de carritos
Al cierre del día, Ana quiere fusionar todos los carritos pendientes con sum(lista_de_carritos). Tal cual, falla: sum empieza sumando 0 + primer_carrito, y ni int ni nuestro __add__ saben resolver eso. Implementa en Carrito el método mágico __radd__ (suma reflejada) mínimo para que funcione: debe aceptar el caso 0 + carrito devolviendo el propio carrito, y delegar con NotImplemented en cualquier otro.
Soluciones
Solución 1:
class Socio:
def __init__(self, nombre, codigo):
self.nombre = nombre
self.codigo = codigo
def __repr__(self):
return f"Socio({self.nombre!r}, {self.codigo!r})"
def __str__(self):
return f"{self.nombre} ({self.codigo})"
def __eq__(self, otro):
if not isinstance(otro, Socio):
return NotImplemented
return self.codigo == otro.codigo
print(Socio("Luis", "LUIS-001") == Socio("Luís", "LUIS-001")) # TrueEl código de socio es el identificador de negocio; el nombre es solo presentación. Decidir qué significa la igualdad es diseño, no sintaxis.
Solución 2:
class Biblioteca:
def __init__(self, nombre, libros=None):
self.nombre = nombre
self._libros = list(libros) if libros else []
def __len__(self):
return sum(libro.stock for libro in self._libros)
def __contains__(self, buscado):
return any(libro == buscado for libro in self._libros)
def __str__(self):
return f"Biblioteca {self.nombre}: {len(self)} ejemplares de {len(self._libros)} títulos"
papyrus = Biblioteca("Papyrus", [
Libro("La Odisea", 12.50, 4), Libro("Hamlet", 9.95, 6),
Libro("El Quijote", 15.90, 8), Libro("Fausto", 21.00, 0),
])
print(papyrus) # Biblioteca Papyrus: 18 ejemplares de 4 títulos
print(Libro("fausto", 0) in papyrus) # True (igualdad por título normalizado)Fíjate en que len() responde una pregunta de negocio (ejemplares), no la trivial (títulos): tú decides la semántica, pero documéntala, porque quien lea len(papyrus) asumirá algo.
Solución 3:
class Carrito:
# ... todo lo anterior ...
def __radd__(self, otro):
if otro == 0: # sum() arranca con 0: 0 + primer_carrito
return self
return NotImplemented
total = sum([carrito_julia, carrito_omar])
print(total) # Carrito de Júlia y Omar: 3 artículos, 39.89 €__radd__ (suma reflejada) se invoca cuando el operando izquierdo no sabe sumar con nosotros: 0 + carrito falla en int.__add__ y Python prueba carrito.__radd__(0). Devolver self en ese caso hace que sum() encadene después los __add__ normales.
Conclusión
Deuda saldada: print(odisea) ya habla claro gracias a __str__, con __repr__ cubriendo la depuración; __eq__ fijó la regla de negocio de Papyrus (mismo título normalizado, mismo libro) devolviendo NotImplemented ante extraños; __lt__ dio a sorted() el orden alfabético sin key=; y el Carrito se comporta como una colección de pleno derecho con __len__, __contains__ y un __add__ que crea carritos nuevos sin mutar los originales. También aprendiste el freno: solo operadores con significado indiscutible, y nombres explícitos para todo lo demás. Pero seamos honestos con el precio pagado: entre __init__, __repr__, __eq__ y __lt__, la clase Libro acumula ya un montón de código repetitivo que solo recita sus atributos una y otra vez. Python lo sabe, y trae la solución de serie: las dataclasses, que generan toda esa maquinaria automáticamente en una línea. Con ellas cerraremos el módulo — y dejaremos el catálogo de Papyrus en su forma definitiva.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
