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__; daindex,count,__contains__,__reversed__MutableSequence añadeinsert,append,pop, etc.Mapping requiere__getitem__,__iter__,__len__; dakeys,values,items,getMutableMapping añade__setitem__,__delitem__,pop,updateSetyMutableSet operaciones de conjuntoCallable comprueba si algo es invocableAwaitable,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.
