Los proyectos orientados a objetos suelen necesitar un contrato común para varias implementaciones. Una clase de pagos debe exponer procesar(); un repositorio necesita guardar() y buscar(); un plugin debe declarar métodos obligatorios. El módulo abc en Python proporciona clases base abstractas, métodos requeridos y subclasses virtuales para expresar estos contratos en runtime.
Esta guía explica ABC, ABCMeta, abstractmethod, register(), __subclasshook__() y update_abstractmethods(). Complementa nuestros artículos sobre types en Python, inspect, singledispatch, descriptors y dataclasses.
Qué es una clase abstracta
Una clase abstracta describe comportamiento esperado y puede ofrecer implementación parcial, pero no puede instanciarse mientras existan miembros abstractos sin resolver.
from abc import ABC, abstractmethod
class Almacenamiento(ABC):
@abstractmethod
def guardar(self, clave, valor):
pass
@abstractmethod
def buscar(self, clave):
passLlamar a Almacenamiento() genera TypeError. Una subclase concreta debe implementar todos los miembros abstractos.
Crear una implementación concreta
class AlmacenamientoMemoria(Almacenamiento):
def __init__(self):
self._datos = {}
def guardar(self, clave, valor):
self._datos[clave] = valor
def buscar(self, clave):
return self._datos[clave]
repositorio = AlmacenamientoMemoria()
repositorio.guardar("usuario", "Ana")
print(repositorio.buscar("usuario"))La clase base documenta el contrato e impide crear implementaciones incompletas por accidente.
ABC como clase auxiliar
ABC usa ABCMeta como metaclass. Heredar de ABC es la forma más sencilla de declarar una base abstracta.
Una clase que ya necesita otra metaclass puede tener conflicto. Crea una metaclass compatible derivada de ABCMeta o reconsidera una jerarquía demasiado compleja.
abstractmethod
@abstractmethod marca comportamiento obligatorio. Debe ser el decorator más interno al combinarse con classmethod, staticmethod o property.
class Fabrica(ABC):
@classmethod
@abstractmethod
def crear(cls, configuracion):
pass
@staticmethod
@abstractmethod
def validar(configuracion):
passEl orden importa porque abstractmethod marca el descriptor subyacente.
Propiedades abstractas
class Documento(ABC):
@property
@abstractmethod
def titulo(self):
passLa subclase implementa otra propiedad.
class Reporte(Documento):
def __init__(self, titulo):
self._titulo = titulo
@property
def titulo(self):
return self._tituloHelpers antiguos como abstractproperty, abstractclassmethod y abstractstaticmethod continúan por compatibilidad, pero se prefieren las combinaciones modernas.
Los métodos abstractos pueden tener implementación
Un método abstracto no necesita contener solo pass. Puede proporcionar comportamiento común llamado mediante super().
class Exportador(ABC):
@abstractmethod
def exportar(self, datos):
self._validar(datos)
def _validar(self, datos):
if not datos:
raise ValueError("datos vacíos")
class ExportadorJSON(Exportador):
def exportar(self, datos):
super().exportar(datos)
return json.dumps(datos)Este patrón evita repetir validaciones y facilita herencia cooperativa.
Implementaciones parciales
Una subclase puede seguir siendo abstracta.
class AlmacenamientoConLog(Almacenamiento):
def guardar(self, clave, valor):
print("guardando", clave)
return self._guardar_impl(clave, valor)
@abstractmethod
def _guardar_impl(self, clave, valor):
passImplementa parte del contrato e introduce una obligación nueva.
Inspeccionar el estado abstracto
La metaclass mantiene __abstractmethods__.
print(Almacenamiento.__abstractmethods__)
print(AlmacenamientoMemoria.__abstractmethods__)Los frameworks pueden usar inspect.isabstract() como API más clara.
ABCMeta directamente
from abc import ABCMeta
class Servicio(metaclass=ABCMeta):
@abstractmethod
def ejecutar(self):
passLa sintaxis explícita ayuda a comprender metaclasses, pero heredar de ABC suele ser más legible.
Subclases virtuales
register() permite considerar una clase existente como subclase sin modificar su herencia.
class Lector(ABC):
@abstractmethod
def leer(self):
pass
class LectorLegado:
def leer(self):
return "datos"
Lector.register(LectorLegado)
print(issubclass(LectorLegado, Lector))
print(isinstance(LectorLegado(), Lector))El registro no añade métodos, no ejecuta código de la ABC ni obliga a cumplir el contrato. Solo modifica issubclass() e isinstance().
Riesgos del registro virtual
class Incompleto:
pass
Lector.register(Incompleto)
print(isinstance(Incompleto(), Lector)) # TrueRegistra únicamente clases que ya cumplen el contrato de forma semántica y conserva tests compartidos de conformidad.
register() como decorator
@Lector.register
class OtroLector:
def leer(self):
return "otro"La intención queda cerca de la implementación, aunque crea dependencia directa de la ABC.
__subclasshook__()
Una ABC puede reconocer subclasses estructurales sin registro explícito.
class IterablePersonalizado(ABC):
@classmethod
def __subclasshook__(cls, C):
if cls is IterablePersonalizado:
if any("__iter__" in B.__dict__ for B in C.__mro__):
return True
return NotImplementedLas clases que definen __iter__ en su jerarquía pueden ser reconocidas por issubclass().
True, False o NotImplemented
__subclasshook__() puede devolver:
Truepara aceptar;Falsepara rechazar de forma fuerte;NotImplementedpara continuar con las reglas normales.
Prefiere NotImplemented cuando el hook no puede decidir.
Hooks conservadores
La presencia de un nombre no demuestra firma ni semántica compatible. Un método llamado leer puede hacer algo distinto.
Usa hooks en protocolos conocidos y simples. Los contratos ricos son más seguros con herencia explícita, Protocol y tests.
ABC frente a Protocol
Las ABC se centran en relaciones nominales de runtime e implementación compartida. typing.Protocol expresa tipado estructural para verificadores estáticos.
from typing import Protocol
class PuedeLeer(Protocol):
def leer(self) -> str: ...Una clase con método compatible satisface el protocolo sin herencia. Los protocols runtime-checkable ofrecen verificaciones limitadas y no validan firmas profundamente.
ABC frente a duck typing
Python puede simplemente llamar objeto.leer() y manejar AttributeError. Esto reduce acoplamiento.
Usa una ABC cuando aportan valor la documentación central, la implementación compartida, la prohibición de clases incompletas o las verificaciones de runtime.
Herencia múltiple
class Legible(ABC):
@abstractmethod
def leer(self): ...
class Escribible(ABC):
@abstractmethod
def escribir(self, datos): ...
class ArchivoVirtual(Legible, Escribible):
def leer(self):
return self.datos
def escribir(self, datos):
self.datos = datosMantén métodos cooperativos con super() y evita estado conflictivo.
Actualizar abstracción dinámicamente
Añadir una implementación después de crear la clase no recalcula automáticamente su estado. Usa update_abstractmethods().
from abc import update_abstractmethods
class Dinamica(ABC):
@abstractmethod
def ejecutar(self):
pass
Dinamica.ejecutar = lambda self: "ok"
update_abstractmethods(Dinamica)
print(Dinamica().ejecutar())La documentación oficial de abc recomienda esta función para modificaciones dinámicas.
Caches de ABC
ABCMeta guarda caches para acelerar issubclass(). get_cache_token() devuelve un token que cambia cuando se registran subclasses virtuales.
from abc import get_cache_token
antes = get_cache_token()
Lector.register(NuevoLector)
despues = get_cache_token()
assert antes != despuesLos frameworks con caches propios pueden invalidarlos cuando cambia el token.
Contratos de plugins
class Plugin(ABC):
nombre: str
@abstractmethod
def iniciar(self, contexto):
pass
@abstractmethod
def detener(self):
passAl cargar plugins, confirma que la clase sea concreta, valida metadatos y maneja fallos. La herencia no proporciona aislamiento.
Dependencias y tests
class Aplicacion:
def __init__(self, almacenamiento: Almacenamiento):
self.almacenamiento = almacenamientoLos tests pueden proporcionar un fake pequeño. Evita mocks demasiado permisivos que aceptan errores tipográficos.
Evolucionar la API
Añadir un método abstracto nuevo rompe todas las subclasses concretas. Es una modificación incompatible.
Considera una implementación predeterminada, una ABC nueva o una capability separada antes de volver obligatorio un comportamiento.
Errores frecuentes
- Colocar
@abstractmethodfuera de@classmethodo@property. - Suponer que una subclase virtual recibe métodos.
- Registrar clases incompatibles.
- Escribir hooks demasiado amplios.
- Añadir métodos abstractos sin plan de compatibilidad.
- Crear jerarquías profundas para compartir poco.
- Tratar una ABC como aislamiento de plugins.
- Modificar una clase sin
update_abstractmethods().
Buenas prácticas
- Mantén contratos pequeños y cohesivos.
- Proporciona implementación común útil.
- Usa
super()en herencia cooperativa. - Registra subclasses virtuales tras probarlas.
- Devuelve
NotImplementedante duda. - Compara ABC, Protocol y duck typing.
- Trata nuevos abstractos como breaking changes.
- Ejecuta una suite compartida de conformidad.
Conclusión
El módulo abc en Python define contratos de runtime, impide construir clases incompletas y comparte comportamiento entre implementaciones. ABC y abstractmethod cubren la mayoría de casos; el registro virtual y __subclasshook__() permiten integración estructural.
Las clases abstractas funcionan mejor con contratos pequeños, estables y claros. Hooks conservadores, tests de conformidad y evolución cuidadosa mantienen las extensiones explícitas sin convertir el proyecto en una jerarquía rígida.





