ABCs en Python: ABC, abstractmethod, register y clases base abstractas del módulo collections.abc

Las clases base abstractas (ABCs) de Python son una forma de definir interfaces que las subclases están obligadas a implementar. A diferencia de la herencia normal, si olvidas implementar un método abstracto, Python lanza TypeError en el momento de instanciar la clase, no en tiempo de ejecución cuando se llama al método. El módulo abc y el submódulo collections.abc son las herramientas centrales para trabajar con este patrón.

ABC y @abstractmethod básicos

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def hablar(self) -> str:
        """Devuelve el sonido del animal."""
        ...

    @abstractmethod
    def moverse(self) -> str:
        ...

    def describir(self) -> str:
        # Método concreto que pueden usar las subclases
        return f"Soy un {type(self).__name__}: {self.hablar()}"


class Perro(Animal):
    def hablar(self) -> str:
        return "Guau"

    def moverse(self) -> str:
        return "corre"


# Animal()   # TypeError: no se puede instanciar clase abstracta
p = Perro()
print(p.describir())   # Soy un Perro: Guau

@abstractmethod combinado con @property

from abc import ABC, abstractmethod

class Figura(ABC):
    @property
    @abstractmethod
    def area(self) -> float:
        ...

    @property
    @abstractmethod
    def perimetro(self) -> float:
        ...

    def __str__(self) -> str:
        return f"{type(self).__name__}: área={self.area:.2f}, perímetro={self.perimetro:.2f}"


class Circulo(Figura):
    def __init__(self, radio: float):
        self._radio = radio

    @property
    def area(self) -> float:
        import math
        return math.pi * self._radio ** 2

    @property
    def perimetro(self) -> float:
        import math
        return 2 * math.pi * self._radio


c = Circulo(5)
print(c)  # Circulo: área=78.54, perímetro=31.42

@abstractmethod con @classmethod y @staticmethod

from abc import ABC, abstractmethod

class Serializador(ABC):
    @classmethod
    @abstractmethod
    def desde_dict(cls, datos: dict) -> "Serializador":
        """Factory a partir de un diccionario."""
        ...

    @staticmethod
    @abstractmethod
    def formato() -> str:
        """Nombre del formato de serialización."""
        ...

    @abstractmethod
    def a_dict(self) -> dict:
        ...


class SerializadorJSON(Serializador):
    def __init__(self, datos: dict):
        self._datos = datos

    @classmethod
    def desde_dict(cls, datos: dict) -> "SerializadorJSON":
        return cls(datos)

    @staticmethod
    def formato() -> str:
        return "json"

    def a_dict(self) -> dict:
        return self._datos


s = SerializadorJSON.desde_dict({"clave": "valor"})
print(s.formato())   # json
print(s.a_dict())    # {'clave': 'valor'}

register(): registrar implementaciones externas

Con register() puedes declarar que una clase existente —que no hereda de tu ABC— cumple el contrato de la interfaz. Esto es útil para integrar código de terceros sin modificarlo:

from abc import ABC, abstractmethod

class Logeable(ABC):
    @abstractmethod
    def log(self, mensaje: str) -> None:
        ...


class LoggerExterno:
    """Clase de terceros que no hereda de Logeable."""
    def log(self, mensaje: str) -> None:
        print(f"[EXTERNO] {mensaje}")


# Registramos LoggerExterno como implementación virtual de Logeable
Logeable.register(LoggerExterno)

logger = LoggerExterno()
print(isinstance(logger, Logeable))   # True
print(issubclass(LoggerExterno, Logeable))  # True

Nota importante: register() no comprueba que la clase realmente implemente los métodos abstractos. Es una declaración de intención, no una garantía.

ABCs de collections.abc

El módulo collections.abc define ABCs para los tipos de datos estándar de Python. Implementar una o dos operaciones clave te da el resto de métodos gratis por mixin:

from collections.abc import Sequence

class FilaInmutable(Sequence):
    """Secuencia inmutable respaldada por una tupla."""
    def __init__(self, *elementos):
        self._datos = tuple(elementos)

    def __getitem__(self, indice):
        return self._datos[indice]

    def __len__(self) -> int:
        return len(self._datos)


f = FilaInmutable(10, 20, 30, 40)
print(len(f))        # 4
print(f[1])          # 20
print(f[1:3])        # (20, 30)   — slicing gratis por Sequence
print(20 in f)       # True       — __contains__ gratis
print(list(f))       # [10, 20, 30, 40]
print(f.index(30))   # 2          — método gratis
print(f.count(10))   # 1          — método gratis

Mapping personalizado con collections.abc

from collections.abc import Mapping

class MapaCongelado(Mapping):
    """Diccionario de solo lectura."""
    def __init__(self, datos: dict):
        self._datos = dict(datos)

    def __getitem__(self, clave):
        return self._datos[clave]

    def __iter__(self):
        return iter(self._datos)

    def __len__(self) -> int:
        return len(self._datos)


m = MapaCongelado({'a': 1, 'b': 2})
print(m['a'])                 # 1
print(list(m.keys()))         # ['a', 'b']
print(list(m.values()))       # [1, 2]
print(list(m.items()))        # [('a', 1), ('b', 2)]
print(m.get('c', 'no existe'))  # no existe

ABCs principales de collections.abc

Las más utilizadas son:

  • Iterable — requiere __iter__
  • Iterator — requiere __iter__ y __next__
  • Sequence — requiere __getitem__ y __len__; da index, count, __contains__, __reversed__
  • MutableSequence — añade insert, append, pop, etc.
  • Mapping — requiere __getitem__, __iter__, __len__; da keys, values, items, get
  • MutableMapping — añade __setitem__, __delitem__, pop, update
  • Set y MutableSet — operaciones de conjunto
  • Callable — comprueba si algo es invocable
  • Awaitable, Coroutine, AsyncIterable — para código asíncrono

Usar ABCs en las anotaciones de tipo (def procesar(datos: Sequence[int])) en lugar de tipos concretos (list[int]) hace el código más flexible y fácil de testar con mocks.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP