Los métodos mágicos (también conocidos como dunder methods, abreviatura de "double underscore") son métodos especiales de Python que comienzan y terminan con dos guiones bajos. Permiten que tus clases se comporten como tipos nativos del lenguaje, respondiendo a operadores, llamadas de función, iteración, indexación y mucho más. Dominar estos métodos es lo que separa a un desarrollador Python intermedio de un verdadero experto en el lenguaje.
En esta guía completa, aprenderás los métodos mágicos más importantes de Python, desde los fundamentos de creación de objetos hasta técnicas avanzadas de personalización. Cada sección incluye ejemplos prácticos que puedes usar inmediatamente en tus proyectos.
¿Qué Son los Métodos Mágicos?
Los métodos mágicos son la implementación que Python ofrece de lo que otros lenguajes llaman sobrecarga de operadores y polimorfismo. Cuando escribes obj + otro_obj o len(coleccion), Python internamente busca métodos específicos en las clases de esos objetos. El intérprete convierte operaciones comunes en llamadas a métodos con nombres predecibles, todos definidos en la documentación oficial de métodos especiales de Python.
Por ejemplo, cuando usas +, Python llama a __add__. Cuando usas len(), llama a __len__. Esto significa que puedes hacer que tus propias clases respondan a estas mismas operaciones simplemente implementando los métodos adecuados.
Métodos de Creación y Destrucción de Objetos
__new__ y __init__
__new__ es el verdadero constructor de Python — crea y retorna una nueva instancia de la clase. __init__ es el inicializador, responsable de configurar el estado del objeto recién creado. Mientras que __init__ se usa ampliamente, __new__ es más común en patrones como Singleton o al trabajar con clases inmutables.
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, valor):
self.valor = valor
a = Singleton(10)
b = Singleton(20)
print(a is b) # True
print(a.valor) # 20
__del__
El método __del__ se llama cuando el objeto está a punto de ser destruido por el recolector de basura. Úsalo con precaución, ya que no hay garantía de cuándo se ejecutará. Es útil para liberar recursos externos como archivos o conexiones, pero la gestión explícita con context managers (ver __enter__ y __exit__) es casi siempre preferible.
Representación de Cadenas
__str__ y __repr__
Estos son probablemente los métodos mágicos más conocidos y utilizados. __repr__ debe retornar una representación "oficial" y no ambigua del objeto, idealmente una cadena que pueda recrear el objeto. __str__ retorna una representación "informal" legible para el usuario final. Si __str__ no está implementado, Python usa __repr__ como alternativa.
class Usuario:
def __init__(self, nombre, email):
self.nombre = nombre
self.email = email
def __repr__(self):
return f"Usuario(nombre='{self.nombre}', email='{self.email}')"
def __str__(self):
return f"{self.nombre} <{self.email}>"
user = Usuario("María García", "[email protected]")
print(repr(user)) # Usuario(nombre='María García', email='[email protected]')
print(str(user)) # María García <[email protected]>
Real Python tiene una guía excelente sobre métodos mágicos que profundiza aún más en estos conceptos.
__format__
El método __format__ permite personalizar cómo se comporta tu objeto dentro de f-strings y con la función format(). Puedes crear especificaciones de formato personalizadas:
class Moneda:
simbolos = {"BRL": "R$", "USD": "$", "EUR": "€"}
def __init__(self, valor, moneda="USD"):
self.valor = valor
self.moneda = moneda
def __format__(self, spec):
simb = self.simbolos.get(self.moneda, self.moneda)
if spec == "extendido":
return f"{simb} {self.valor:,.2f} ({self.moneda})"
return f"{simb} {self.valor:{spec}f}" if spec else f"{simb} {self.valor:,.2f}"
dinero = Moneda(1250.50)
print(f"{dinero}") # $ 1,250.50
print(f"{dinero:extendido}") # $ 1,250.50 (USD)
Métodos de Comparación
Los métodos de comparación permiten que los objetos se comparen con operadores como ==, <, >, <=, >= y !=. Implementarlos correctamente también habilita la ordenación con sorted().
class Persona:
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
def __eq__(self, other):
if not isinstance(other, Persona):
return NotImplemented
return self.nombre == other.nombre and self.edad == other.edad
def __lt__(self, other):
if not isinstance(other, Persona):
return NotImplemented
return self.edad < other.edad
def __hash__(self):
return hash((self.nombre, self.edad))
def __repr__(self):
return f"{self.nombre} ({self.edad})"
personas = [
Persona("Ana", 30),
Persona("Bruno", 25),
Persona("Carlos", 35),
]
for p in sorted(personas):
print(p) # Bruno (25), Ana (30), Carlos (35)
Implementar __eq__ y __hash__ juntos es esencial si planeas usar tus objetos en conjuntos (set) o como claves de diccionarios. GeeksforGeeks tiene una referencia completa sobre dunder methods con más ejemplos de estos patrones.
Métodos Aritméticos
Python permite sobrecargar prácticamente todos los operadores aritméticos. Esto es especialmente útil para crear tipos numéricos personalizados, vectores, matrices y objetos de dominio específico.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, escalar):
if not isinstance(escalar, (int, float)):
return NotImplemented
return Vector(self.x * escalar, self.y * escalar)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 * 2) # Vector(6, 8)
print(abs(v1)) # 5.0
Además de los operadores básicos, existen métodos para operadores compuestos como __iadd__ (+=), __isub__ (-=), operadores unarios como __neg__ y __pos__, y operadores reflejados. Consulta el módulo operator de Python para la lista completa.
Métodos de Contenedor
Con estos métodos, puedes hacer que tus clases se comporten como listas, diccionarios o conjuntos. Son la base del protocolo de contenedores en Python.
class ListaPersonalizada:
def __init__(self, *items):
self._items = list(items)
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, valor):
self._items[index] = valor
def __delitem__(self, index):
del self._items[index]
def __contains__(self, item):
return item in self._items
def __iter__(self):
return iter(self._items)
def __reversed__(self):
return reversed(self._items)
def __repr__(self):
return f"ListaPersonalizada({self._items!r})"
lista = ListaPersonalizada(10, 20, 30, 40, 50)
print(len(lista)) # 5
print(30 in lista) # True
print(lista[1:3]) # [20, 30]
lista[0] = 100
print(lista) # ListaPersonalizada([100, 20, 30, 40, 50])
Implementar el protocolo de contenedor completo hace que tu clase funcione perfectamente con bucles for, comprensiones de listas y la mayoría de las funciones integradas que esperan secuencias.
Objetos Invocables con __call__
El método __call__ permite que las instancias de una clase sean invocadas como funciones. Es extremadamente útil para crear funciones con estado (clases que actúan como closures), decoradores basados en clases y fábricas.
from time import perf_counter
class Temporizador:
def init(self):
self.tiempos = []
def __call__(self, func):
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
inicio = perf_counter()
resultado = func(*args, **kwargs)
fin = perf_counter()
self.tiempos.append((func.__name__, fin - inicio))
print(f"{func.__name__} ejecutó en {fin - inicio:.4f}s")
return resultado
return wrapper
timer = Temporizador()
@timer
def procesar_datos():
return sum(range(1_000_000))
@timer
def otra_funcion():
return sum(i * 2 for i in range(500_000))
procesar_datos()
otra_funcion()
print(timer.tiempos)
Los objetos invocables son una alternativa elegante a las funciones en muchos escenarios, especialmente cuando necesitas mantener estado entre llamadas.
Context Managers: __enter__ y __exit__
Los context managers permiten usar la sintaxis with para gestionar recursos. Implementar __enter__ y __exit__ en tu clase permite crear context managers personalizados para conexiones de bases de datos, archivos, transacciones y más.
class ConexionBaseDatos:
def __init__(self, cadena_conexion):
self.cadena_conexion = cadena_conexion
self.conexion = None
def __enter__(self):
print(f"Conectando a {self.cadena_conexion}...")
self.conexion = {"conectado": True, "url": self.cadena_conexion}
return self.conexion
def __exit__(self, exc_type, exc_val, exc_tb):
if self.conexion:
print("Cerrando conexión...")
self.conexion["conectado"] = False
if exc_type:
print(f"Error manejado: {exc_val}")
return True # Suprime excepciones
with ConexionBaseDatos("postgresql://localhost:5432/mydb") as conn:
print("Operaciones con base de datos...")
raise ValueError("¡Algo salió mal!")
print("Continuó después de la excepción manejada")
Retornar True desde __exit__ suprime excepciones. Si retornas False o None, la excepción se propaga. Esta es una de las funcionalidades más importantes para escribir código robusto e idiomático en Python.
Control de Acceso a Atributos
Los métodos __getattr__, __setattr__, __delattr__ y __getattribute__ ofrecen control detallado sobre cómo se accede y modifican los atributos. Usa __getattr__ para proporcionar valores predeterminados para atributos inexistentes y __setattr__ para validar o transformar valores antes de almacenarlos.
class Validado:
def __init__(self):
self.__dict__["_datos"] = {}
def __getattr__(self, nombre):
if nombre in self._datos:
return self._datos[nombre]
raise AttributeError(f"'{type(self).__name__}' no tiene atributo '{nombre}'")
def __setattr__(self, nombre, valor):
if nombre.startswith("_"):
self.__dict__[nombre] = valor
elif isinstance(valor, (int, float)) and valor < 0:
raise ValueError(f"{nombre} no puede ser negativo")
else:
self._datos[nombre] = valor
obj = Validado()
obj.nombre = "Python"
obj.edad = 25
print(obj.nombre) # Python
obj.edad = -5 # ValueError: edad no puede ser negativo
Cuidado con los bucles infinitos al usar __setattr__ — usa self.__dict__ directamente para evitar recursión. La documentación oficial sobre acceso a atributos detalla todos los matices de este mecanismo.
Conversión de Tipo: __int__, __float__, __bool__
Estos métodos permiten que tus objetos se conviertan a tipos nativos usando funciones como int(), float() y bool(). El método __bool__ es particularmente importante, ya que determina cómo se comporta el objeto en contextos booleanos como if, while y operadores lógicos.
class Puntuacion:
def __init__(self, valor, maximo=100):
self.valor = valor
self.maximo = maximo
def __int__(self):
return int(self.valor)
def __float__(self):
return float(self.valor)
def __bool__(self):
return self.valor > 0
def __lt__(self, other):
if isinstance(other, (int, float)):
return self.valor < other
return NotImplemented
p = Puntuacion(85)
print(int(p)) # 85
print(float(p)) # 85.0
print(bool(p)) # True
print(p > 50) # True
__slots__: Optimización de Memoria
El atributo especial __slots__ no es exactamente un método mágico, pero es un mecanismo poderoso para ahorrar memoria. Al declarar __slots__, previenes la creación del diccionario __dict__ en cada instancia, reduciendo significativamente el consumo de memoria para objetos que existen en grandes cantidades.
class Punto:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Punto(10, 20)
print(p.x, p.y) # 10 20
p.z = 30 # AttributeError: 'Punto' object has no attribute 'z'
En aplicaciones que crean millones de objetos (como procesamiento de datos o videojuegos), el uso de __slots__ puede reducir el uso de memoria hasta en un 50%, según la documentación oficial sobre __slots__.
Data Classes vs Métodos Mágicos
Desde Python 3.7, las dataclasses (documentadas en PEP 557) automatizan la implementación de varios métodos mágicos como __init__, __repr__, __eq__ y __hash__. Para clases simples que son principalmente contenedores de datos, las dataclasses suelen ser la mejor opción:
from dataclasses import dataclass
@dataclass
class Producto:
nombre: str
precio: float
cantidad: int = 0
@property
def valor_total(self) -> float:
return self.precio * self.cantidad
p1 = Producto("Laptop", 3500.00, 5)
p2 = Producto("Laptop", 3500.00, 5)
print(p1) # Producto(nombre='Laptop', precio=3500.0, cantidad=5)
print(p1 == p2) # True (eq automático)
Para casos más complejos con lógica de negocio, validación o comportamiento específico, implementar manualmente los métodos mágicos sigue siendo el enfoque más flexible.
Buenas Prácticas y Errores Comunes
1. Siempre retorna NotImplemented en lugar de lanzar TypeError: Cuando un método mágico no sabe cómo manejar el tipo recibido, retorna NotImplemented. Esto permite que Python intente la operación inversa en el otro operando.
2. Implementa __hash__ cuando implementes __eq__: Los objetos que implementan __eq__ sin __hash__ se vuelven no hashables y no pueden usarse en sets ni como claves de diccionario.
3. Mantén __repr__ sin ambigüedad y __str__ legible: La convención es clara: __repr__ para el desarrollador, __str__ para el usuario final.
4. Cuidado con efectos secundarios en __del__: El recolector de basura de Python no garantiza cuándo se llamará a __del__. Usa context managers para liberación determinística de recursos.
5. Prefiere decoradores @property sobre __getattr__: Para acceso controlado a atributos específicos, @property es más claro y predecible que __getattr__. Consulta la documentación de la función property para más detalles.
6. Evita __slots__ prematuramente: Usa __slots__ solo cuando tengas un número muy grande de instancias y necesites optimizar el uso de memoria. La optimización prematura añade complejidad innecesaria.
Conclusión
Los métodos mágicos son una de las características más poderosas de Python. Te permiten crear clases que se integran perfectamente con la sintaxis y los protocolos del lenguaje, dando como resultado código más limpio, expresivo e idiomático.
Dominar __init__, __str__, __repr__, los métodos de comparación, operadores aritméticos, el protocolo de contenedor y context managers transformará la calidad de tu código Python. Empieza implementando estos métodos en tus clases gradualmente — verás la diferencia en legibilidad y flexibilidad de inmediato.
Para continuar tus estudios en Python, consulta nuestra guía completa sobre Programación Orientada a Objetos en Python y el tutorial de Decoradores en Python, que complementan perfectamente lo que aprendiste aquí sobre métodos mágicos.
Y recuerda: la documentación oficial es tu mejor amiga. Mantén la página de Special Method Names en la Documentación de Python siempre a mano — es la referencia definitiva sobre métodos mágicos en Python.