CAP 09 · LEC 04·Programación orientada a objetos

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.

● INTERMEDIO10 min lectura5 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

__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__
SalidaTrue {'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 fallback
SalidaPoint(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)
Implementa siempre __repr__

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]
SalidaTrue 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) # True
Salida3 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")
Salidasuma de cuadrados: 0.0821s Resultado: 333332833333500000 Duración guardada: 0.0821s

Practica