Kodokon kodokon.com

Protocolos y métodos especiales: __repr__, __eq__, gestores de contexto

Domina el modelo de objetos de Python: el contrato __eq__/__hash__, el valor NotImplemented, los gestores de contexto y el tipado estructural con Protocol.

9 min · 3 preguntas

Abrir esta lección en Kodokon

En Python, cada operador, cada conversión, cada instrucción with pasa por un método especial: a == b llama a a.__eq__(b), len(x) llama a x.__len__(). Ten cuidado de distinguir __repr__ - la representación sin ambigüedades destinada a quien programa, la que se muestra en el depurador y dentro de las listas - de __str__, destinada al usuario final. Si falta __str__, Python recurre a __repr__: implementa por tanto siempre __repr__ primero, idealmente en una forma que permita reconstruir el objeto.

PYTHON
class Money:
    def __init__(self, amount: int, currency: str):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount!r}, {self.currency!r})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Money):
            return NotImplemented
        return (self.amount, self.currency) == (
            other.amount, other.currency)

print(Money(10, "EUR") == Money(10, "EUR"))
print(Money(10, "EUR") == "10 EUR")
Comparar con un tipo desconocido devuelve NotImplemented, nunca False.

Devolver NotImplemented (el valor, que no debe confundirse con la excepción NotImplementedError) activa el protocolo de negociación de operadores: Python intenta entonces la operación reflejada other.__eq__(self), y solo concluye False como último recurso. Devolver False directamente cortocircuitaría esa negociación y rompería las comparaciones con subclases o con tipos de terceros que sí sabrían responder.

PYTHON
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb):
        stop = time.perf_counter()
        self.elapsed = stop - self.start
        return False

with Timer() as timer:
    sum(range(1_000_000))
print(f"{timer.elapsed:.4f} s")
Devolver False deja que cualquier excepción se propague normalmente.

Si __exit__ devuelve True, la excepción actual queda suprimida - así es exactamente como funciona contextlib.suppress. Para evitar escribir una clase completa, @contextlib.contextmanager convierte un generador en un gestor de contexto: el código anterior al yield desempeña el papel de __enter__, y el código posterior (idealmente dentro de un finally) el de __exit__. Otro pilar del modelo de objetos: los tipos Protocol del módulo typing formalizan el duck typing - una clase es compatible si expone los métodos adecuados, sin ninguna herencia declarada.

PYTHON
from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

class Session:
    def close(self) -> None:
        print("session closed")

def shutdown(resource: Closable) -> None:
    resource.close()

shutdown(Session())
print(isinstance(Session(), Closable))
Session no declara ninguna herencia: la conformidad es estructural.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. En __eq__, ¿qué debes devolver ante un tipo con el que no puedes compararte?
    • False, para señalar claramente la desigualdad
    • Lanzar NotImplementedError
    • NotImplemented, para dejar que Python intente la operación reflejada del otro operando
    • None, que Python convertirá en False
  2. Añades __eq__ a una clase sin tocar __hash__. ¿Cuál es la consecuencia?
    • Ninguna: __hash__ se sigue heredando de object
    • __hash__ pasa a valer None: las instancias ya no pueden usarse como claves de diccionario ni como elementos de un set
    • El hash se convierte automáticamente en el de la tupla de atributos
  3. ¿Qué significa que un método __exit__ devuelva True?
    • El bloque with terminó sin error
    • El gestor de contexto puede reutilizarse
    • Python volverá a ejecutar el bloque with una segunda vez
    • La excepción actual queda suprimida y la ejecución continúa después del bloque with