Dunder methods: __init__, __repr__, __eq__, __len__ y más
Los métodos dunder (double underscore) son los que Python llama automáticamente en respuesta a operadores y funciones built-in. Implementarlos correctamente hace que tus clases se comporten como tipos nativos.
__init__ y __new__
__init__ es el inicializador — recibe el objeto ya creado (self) y lo configura. __new__ es el constructor real — crea y retorna el objeto. En la práctica, solo necesitas __new__ en casos avanzados como singletons o subclases de tipos inmutables.
class Singleton:
"""Ejemplo clásico de __new__ para garantizar una sola instancia."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
# __init__ se llama cada vez, pero siempre es la misma instancia
if not hasattr(self, "initialized"):
self.data: dict = {}
self.initialized = True
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True — misma instancia
s1.data["key"] = "valor"
print(s2.data) # {'key': 'valor'} — mismo objeto
# Para la mayoría de clases, __new__ no se toca — solo __init__
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
# Python llama a __new__ automáticamente antes de __init__True
{'key': 'valor'}__repr__ vs __str__
__repr__ está pensado para desarrolladores — debe ser una representación inequívoca, idealmente que pueda pasarse a eval(). __str__ está pensado para el usuario final — puede ser más legible. print() usa __str__; la REPL usa __repr__.
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
# Formato que puede usarse para recrear el objeto
return f"Point({self.x!r}, {self.y!r})"
def __str__(self) -> str:
# Formato amigable para el usuario final
return f"({self.x}, {self.y})"
p = Point(3.0, 4.0)
print(repr(p)) # Point(3.0, 4.0) — usa __repr__
print(str(p)) # (3.0, 4.0) — usa __str__
print(p) # (3.0, 4.0) — print usa __str__
# En una lista, Python usa __repr__
puntos = [Point(1, 2), Point(3, 4)]
print(puntos) # [Point(1, 2), Point(3, 4)]
# Si no hay __str__, Python usa __repr__ como fallback
class Color:
def __init__(self, r: int, g: int, b: int) -> None:
self.r, self.g, self.b = r, g, b
def __repr__(self) -> str:
return f"Color(r={self.r}, g={self.g}, b={self.b})"
c = Color(255, 128, 0)
print(c) # Color(r=255, g=128, b=0) — usa __repr__ como fallbackPoint(3.0, 4.0)
(3.0, 4.0)
(3.0, 4.0)
[Point(1, 2), Point(3, 4)]
Color(r=255, g=128, b=0)Si solo implementas uno, implementa __repr__. Es útil en debugging, logging y en la REPL. __str__ puede esperar hasta que tengas claro cómo quieres presentar el objeto al usuario.
__eq__, __lt__, __le__ (comparación)
Para que tus objetos soporten operadores de comparación (==, <, <=, etc.), implementa los métodos dunder correspondientes. Si implementas __eq__, Python también pierde el __hash__ por defecto — necesitas definirlo si quieres usar el objeto en sets o como clave de dict.
class Version:
"""Representa una versión semántica: mayor.menor.parche"""
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def _tuple(self) -> tuple:
return (self.major, self.minor, self.patch)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._tuple() == other._tuple()
def __lt__(self, other: "Version") -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._tuple() < other._tuple()
def __le__(self, other: "Version") -> bool:
return self == other or self < other
def __hash__(self) -> int:
return hash(self._tuple())
def __repr__(self) -> str:
return f"v{self.major}.{self.minor}.{self.patch}"
v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 3)
print(v1 == v3) # True
print(v1 < v2) # True
print(v2 > v1) # True (Python invierte: v2.__gt__ usa v1.__lt__)
print(sorted([v2, v1, v3])) # [v1.2.3, v1.2.3, v1.3.0]True
True
True
[v1.2.3, v1.2.3, v1.3.0]__len__ y __getitem__ (secuencias)
Implementar __len__ permite usar len() en tus objetos. Implementar __getitem__ permite acceso por índice con obj[i] y hace que el objeto sea iterable por defecto.
class Playlist:
def __init__(self, name: str) -> None:
self.name = name
self._tracks: list[str] = []
def add(self, track: str) -> None:
self._tracks.append(track)
def __len__(self) -> int:
return len(self._tracks)
def __getitem__(self, index: int) -> str:
return self._tracks[index]
def __repr__(self) -> str:
return f"Playlist('{self.name}', {len(self)} tracks)"
pl = Playlist("Mis favoritas")
pl.add("Bohemian Rhapsody")
pl.add("Hotel California")
pl.add("Stairway to Heaven")
print(len(pl)) # 3
print(pl[0]) # Bohemian Rhapsody
print(pl[-1]) # Stairway to Heaven
print(pl[1:3]) # ['Hotel California', 'Stairway to Heaven']
# Al tener __getitem__, Python puede iterar sin __iter__
for track in pl:
print(f" ♫ {track}")
# También funciona con in
print("Hotel California" in pl) # True3
Bohemian Rhapsody
Stairway to Heaven
['Hotel California', 'Stairway to Heaven']
♫ Bohemian Rhapsody
♫ Hotel California
♫ Stairway to Heaven
True__enter__ y __exit__ (context manager básico)
Implementar __enter__ y __exit__ convierte tu clase en un context manager, usable con with. Es el patrón correcto para recursos que necesitan limpieza garantizada (ficheros, conexiones, locks).
class Timer:
"""Context manager que mide el tiempo de un bloque de código."""
import time
def __init__(self, name: str = "bloque") -> None:
self.name = name
self.elapsed: float = 0.0
def __enter__(self) -> "Timer":
import time
self._start = time.perf_counter()
return self # el valor retornado es el 'as' en el with
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
import time
self.elapsed = time.perf_counter() - self._start
print(f"{self.name}: {self.elapsed:.4f}s")
# Retornar True suprime excepciones; False las propaga
return False
with Timer("suma de cuadrados") as t:
resultado = sum(i ** 2 for i in range(1_000_000))
print(f"Resultado: {resultado}")
print(f"Duración guardada: {t.elapsed:.4f}s")suma de cuadrados: 0.0821s
Resultado: 333332833333500000
Duración guardada: 0.0821s