W3docs

Métodos mágicos (dunder) de Python

Aprende los métodos mágicos de Python (__init__, __str__, __repr__), sobrecarga de operadores, protocolos de contenedor y gestor de contexto con ejemplos prácticos.

Los métodos mágicos — también llamados métodos dunder (abreviatura de doble guion bajo) — son métodos especiales cuyos nombres comienzan y terminan con dos guiones bajos, como __init__ o __len__. Son el sistema de hooks de Python: al definirlos en tus propias clases le dices a Python cómo debe comportarse un objeto con operadores y funciones integradas como +, len(), print(), in y with.

Nunca llamas a los métodos dunder directamente. En su lugar, Python los llama automáticamente entre bastidores:

Expresión PythonDunder llamado
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__contains__(item)
with obj as x:obj.__enter__() / obj.__exit__(...)

Este capítulo cubre:

  • Representación como string — __repr__ y __str__
  • Operadores de comparación — __eq__, __lt__ y similares
  • Operadores aritméticos — __add__, __mul__, __rmul__ y más
  • Protocolo de contenedor — __len__, __getitem__, __contains__
  • Protocolo de iterador — __iter__ y __next__
  • Veracidad — __bool__
  • Objetos invocables — __call__
  • Protocolo de gestor de contexto — __enter__ y __exit__
  • __hash__ — hacer que los objetos sean utilizables como claves de diccionario

Antes de leer, asegúrate de estar familiarizado con las clases y objetos de Python y la herencia en Python. Para atributos calculados, consulta @property.

Representación como string: __repr__ y __str__

Estos dos métodos controlan cómo se convierte un objeto en una cadena.

MétodoLlamado porPropósito
__repr__repr(), shell interactivoRepresentación inequívoca orientada al desarrollador
__str__str(), print(), f-stringsVisualización amigable para el usuario

Si __str__ no está definido, Python recurre a __repr__. Por lo tanto, es una buena práctica siempre definir __repr__ y definir __str__ solo cuando se quiere un formato legible diferente para el usuario.

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def __repr__(self):
        return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"

    def __str__(self):
        return f'"{self.title}" by {self.author} ({self.pages} pages)'

b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b))  # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b))   # "Clean Code" by Robert C. Martin (431 pages)
print(b)        # "Clean Code" by Robert C. Martin (431 pages)

La bandera de conversión !r dentro de una f-string llama a repr() sobre ese valor, lo que envuelve las cadenas entre comillas. Esto hace que la salida de __repr__ sea código Python que se puede copiar y pegar.

Consejo: un buen __repr__ permite reconstruir el objeto a partir de su salida. Piénsalo como eval(repr(obj)) == obj como modelo mental (aunque no sea literalmente cierto).

Operadores de comparación

Todos los operadores de comparación de Python se asignan a métodos dunder. Defínelos cuando quieras que ==, <, >, <= o >= comparen tus objetos de forma significativa.

OperadorMétodoMétodo reflejado
==__eq____eq__
!=__ne____ne__
<__lt____gt__
<=__le____ge__
>__gt____lt__
>=__ge____le__

Reflejado significa que Python intenta el método del operando derecho cuando el operando izquierdo devuelve NotImplemented. Por ejemplo, si a < b llama a a.__lt__(b) y esto devuelve NotImplemented, Python intenta entonces el método reflejado: b.__gt__(a).

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius

    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

    def __le__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius <= other.celsius

    def __repr__(self):
        return f"Temperature({self.celsius}°C)"

t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)

print(t1 == t3)  # True
print(t1 < t2)   # True
print(t2 > t1)   # True  — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3)  # True

Atajo: si solo quieres que los objetos sean ordenables sin preocuparte por los seis operadores individuales, usa el decorador @functools.total_ordering. Define __eq__ y uno de __lt__, __le__, __gt__ o __ge__, y total_ordering completa el resto automáticamente.

from functools import total_ordering

@total_ordering
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius

    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

Operadores aritméticos

Los dunders aritméticos permiten que tus objetos funcionen con +, -, *, /, //, % y **.

ExpresiónMétodoNotas
a + b__add__
a - b__sub__
a * b__mul__
b * a__rmul__versión del operando derecho; se llama cuando b.__mul__(a) devuelve NotImplemented
-a__neg__negación unaria
abs(a)__abs__
a += b__iadd__en el lugar; recurre a __add__ si no está definido

Un caso de uso clásico es una clase de vector 2D:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar):   # supports: 3 * v
        return self.__mul__(scalar)

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)  # Vector(4, 6)
print(v2 - v1)  # Vector(2, 2)
print(v1 * 3)   # Vector(3, 6)
print(3 * v1)   # Vector(3, 6)  — uses __rmul__
print(-v1)      # Vector(-1, -2)
print(abs(v2))  # 5.0

__rmul__ es lo que permite que 3 * v1 funcione. Cuando Python evalúa 3 * v1, primero llama a int.__mul__(3, v1). La clase entera integrada no sabe cómo multiplicar un entero por un Vector, por lo que devuelve NotImplemented. Python entonces intenta el método reflejado: v1.__rmul__(3), que tiene éxito.

Protocolo de contenedor

Implementa estos métodos para que tu clase se comporte como una secuencia o colección.

MétodoLlamado porLo que habilita
__len__len(obj)Longitud del contenedor
__getitem__obj[index]Acceso por índice y por slice
__setitem__obj[index] = valAsignación por índice
__delitem__del obj[index]Eliminación por índice
__contains__item in objPrueba de pertenencia

Definir __len__ junto con __getitem__ es suficiente para que tu clase sea iterable automáticamente — el bucle for de Python llamará a __getitem__ con índices sucesivos comenzando desde 0 hasta obtener un IndexError.

class WordBag:
    def __init__(self, *words):
        self._words = list(words)

    def __len__(self):
        return len(self._words)

    def __contains__(self, item):
        return item in self._words

    def __getitem__(self, index):
        return self._words[index]

    def __repr__(self):
        return f"WordBag({self._words!r})"

bag = WordBag("apple", "banana", "cherry")

print(len(bag))         # 3
print("banana" in bag)  # True
print("grape" in bag)   # False
print(bag[0])           # apple
print(bag[-1])          # cherry

# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
    print(word)
# apple
# banana
# cherry

Protocolo de iterador

Si quieres un comportamiento de iterador completo (funcionar con iter() y next() directamente, o ser utilizable en lugares que requieren un iterador en lugar de solo un iterable), define tanto __iter__ como __next__:

  • __iter__ — llamado por iter(obj) y al inicio de un bucle for; debe devolver el objeto iterador (generalmente self).
  • __next__ — llamado repetidamente para producir el siguiente valor; debe lanzar StopIteration cuando se agote.
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        self.current = self.start
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

for n in Countdown(3):
    print(n)
# 3
# 2
# 1
# 0

Para patrones de iteración más potentes — especialmente secuencias perezosas que producen valores bajo demanda — consulta Generadores de Python e Iteradores de Python.

Veracidad: __bool__

Python llama a __bool__ cuando un objeto se usa en un contexto boolean (una sentencia if, bucle while, not, and, or). Si __bool__ no está definido pero __len__ sí lo está, Python usa len(obj) != 0 como valor de verdad. Si ninguno está definido, el objeto siempre es verdadero.

class Stack:
    def __init__(self):
        self._data = []

    def push(self, item):
        self._data.append(item)

    def pop(self):
        return self._data.pop()

    def __len__(self):
        return len(self._data)

    def __bool__(self):
        return len(self._data) > 0

    def __repr__(self):
        return f"Stack({self._data!r})"

s = Stack()
print(bool(s))  # False — empty stack is falsy

s.push(1)
print(bool(s))  # True
print(len(s))   # 1

if s:
    print("stack has items")  # stack has items

Esto refleja cómo funcionan las colecciones integradas: una lista, dict o set vacíos son falsos; uno no vacío es verdadero.

Objetos invocables: __call__

Definir __call__ te permite usar una instancia como si fuera una función. Esto es útil para objetos que mantienen estado entre llamadas — algo que una función simple no puede hacer sin una clausura o una variable global.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))       # 10
print(triple(5))       # 15
print(callable(double))  # True

double y triple son objetos ordinarios, pero se llaman con () igual que las funciones. La función integrada callable() devuelve True para cualquier objeto que tenga __call__.

Este patrón es común en los frameworks de aprendizaje automático (capas, funciones de pérdida) y en las fábricas de decoradores. Consulta Decoradores de Python para un caso de uso estrechamente relacionado.

Protocolo de gestor de contexto: __enter__ y __exit__

La sentencia with es la forma que tiene Python de configurar y liberar un recurso de manera fiable, incluso si ocurre una excepción. Cualquier objeto que defina __enter__ y __exit__ puede usarse como gestor de contexto.

  • __enter__(self) — se ejecuta cuando el bloque with comienza; su valor de retorno se vincula a la variable as.
  • __exit__(self, exc_type, exc_val, exc_tb) — se ejecuta cuando el bloque termina, ya sea normalmente o mediante una excepción. Devuelve True para suprimir la excepción; devuelve False (o None) para permitir que se propague.
class ManagedFile:
    def __init__(self, path, mode="r"):
        self.path = path
        self.mode = mode
        self._file = None

    def __enter__(self):
        self._file = open(self.path, self.mode)
        return self._file   # the value bound to the "as" variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._file:
            self._file.close()
        return False  # do not suppress exceptions

with ManagedFile("/etc/hostname") as f:
    content = f.read()

# The file is guaranteed to be closed here, even if an exception occurred inside the block.

El decorador contextlib.contextmanager de la biblioteca estándar te permite escribir la misma lógica como una función generadora — una alternativa más ligera para casos simples. Consulta Sentencia with de Python para una cobertura completa.

Hashing: __hash__

Python usa __hash__ para colocar objetos en conjuntos y diccionarios. El __hash__ predeterminado se basa en la dirección de memoria del objeto (identidad). Cuando sobreescribes __eq__, Python establece automáticamente __hash__ en None, haciendo que tus objetos no sean hashables — debes definir __hash__ explícitamente si aún quieres que funcionen en conjuntos o como claves de dict.

La regla: los objetos que se comparan como iguales deben tener el mismo hash.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))  # hash of an immutable tuple

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)             # True
print(p1 is p2)             # False — different objects in memory
print(hash(p1) == hash(p2)) # True

seen = {p1, p2, p3}
print(len(seen))   # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen)  # True

Si tu clase es mutable (sus campos pueden cambiar después de la creación), no definas __hash__. Los objetos mutables no deben ser hashables porque cambiar sus campos cambiaría su hash, rompiendo cualquier conjunto o dict que ya los contenga.

Tabla resumen

CategoríaMétodoActivado por
Representación__repr__repr(obj), shell interactivo
Representación__str__str(obj), print(obj), f-strings
Comparación__eq__, __ne__==, !=
Comparación__lt__, __le__, __gt__, __ge__<, <=, >, >=
Aritmética__add__, __sub__, __mul__+, -, *
Aritmética__rmul__, __radd__, …formas reflejadas del operando derecho
Aritmética__neg__, __abs__- unario, abs()
Contenedor__len__len(obj)
Contenedor__getitem__, __setitem__, __delitem__obj[i], obj[i] = v, del obj[i]
Contenedor__contains__item in obj
Iterador__iter__iter(obj), bucle for
Iterador__next__next(obj)
Veracidad__bool__bool(obj), if obj:
Invocable__call__obj(args)
Gestor de contexto__enter__, __exit__with obj as x:
Hashing__hash__hash(obj), claves de dict, conjuntos

Cuándo usar los métodos mágicos

  • Úsalos cuando tu clase representa un tipo valor (un punto, un vector, una cantidad monetaria, un rango de fechas) — sobrecargar operadores y comparaciones hace que la clase se sienta natural.
  • Úsalos cuando tu clase envuelve un recurso (un archivo, una conexión de base de datos, un socket de red) — __enter__/__exit__ garantiza que el recurso siempre se libere.
  • Úsalos cuando tu clase es una colección personalizada — los protocolos de contenedor e iterador le permiten funcionar con for, in, len() y comprensiones de listas.
  • Evítalos para clases de aplicación ordinarias que no son tipos valor ni contenedores. Sobrecargar + en una clase User resultaría confuso.

Para patrones OOP más avanzados, consulta Clases abstractas de Python, Encapsulación en Python y Polimorfismo en Python.

Práctica

Práctica
¿Qué método dunder llama Python cuando usas un objeto en un contexto boolean como 'if obj:'?
¿Qué método dunder llama Python cuando usas un objeto en un contexto boolean como 'if obj:'?
Was this page helpful?