Clases Abstractas en Python
¿Qué es una clase abstracta?
Una clase abstracta es una clase que no puedes instanciar directamente. Existe para ser heredada. Define la estructura —qué métodos deben existir— pero delega la implementación a las subclases. Es, en esencia, un contrato: quien firme el contrato (quien herede) se compromete a implementar lo que se le exige.
La idea es tan antigua como la programación orientada a objetos, pero en Python cobra un matiz especial: el lenguaje no obliga a declarar tipos, y sin clases abstractas cualquiera puede heredar de tu clase y olvidarse de implementar un método crítico. El error aparecerá en tiempo de ejecución, en el peor momento posible. Las clases abstractas convierten ese error tardío en un error inmediato y descriptivo en el momento de la instanciación.
Circulo y Rectangulo son sombras en la caverna; FiguraGeometrica es la forma real.Python implementa las clases abstractas a través del módulo abc (Abstract Base Classes) de la biblioteca estándar. Su uso es elegante y muy pythonico.
ABC y @abstractmethod
Para crear una clase abstracta necesitas dos cosas: heredar de ABC y marcar los métodos obligatorios con el decorador @abstractmethod.
from abc import ABC, abstractmethod
class FiguraGeometrica(ABC):
"""Clase abstracta para todas las figuras geométricas."""
@abstractmethod
def area(self) -> float:
"""Devuelve el área de la figura."""
...
@abstractmethod
def perimetro(self) -> float:
"""Devuelve el perímetro de la figura."""
...
Los tres puntos (...) son la convención para indicar que el cuerpo del método es intencionalmente vacío. También puedes usar pass. Algunos desarrolladores prefieren incluir un docstring en lugar del ... para documentar qué debe hacer el método.
Intenta instanciar esta clase y Python te detiene en seco:
f = FiguraGeometrica()
# TypeError: Can't instantiate abstract class FiguraGeometrica
# with abstract methods area, perimetro
El mensaje de error es explícito: te dice exactamente qué métodos faltan. Eso es mucho más útil que un AttributeError genérico apareciendo tres llamadas después.
FiguraGeometrica actúa como contrato. Circulo y Rectangulo implementan todos los métodos: instanciables. FiguraIncompleta omite perimetro(): Python lanza TypeError.Implementar una subclase concreta
Una subclase es "concreta" cuando implementa todos los métodos abstractos. Hasta ese momento, sigue siendo abstracta de facto.
import math
class Circulo(FiguraGeometrica):
def __init__(self, radio: float):
self.radio = radio
def area(self) -> float:
return math.pi * self.radio ** 2
def perimetro(self) -> float:
return 2 * math.pi * self.radio
class Rectangulo(FiguraGeometrica):
def __init__(self, ancho: float, alto: float):
self.ancho = ancho
self.alto = alto
def area(self) -> float:
return self.ancho * self.alto
def perimetro(self) -> float:
return 2 * (self.ancho + self.alto)
Ahora sí puedes instanciarlas:
c = Circulo(5)
print(c.area()) # 78.53981633974483
print(c.perimetro()) # 31.41592653589793
r = Rectangulo(4, 6)
print(r.area()) # 24
print(r.perimetro()) # 20
Y lo mejor: puedes usarlas de forma polimórfica. Cualquier función que espere una FiguraGeometrica funcionará con cualquier subclase sin cambiar una línea de código:
def imprimir_info(figura: FiguraGeometrica):
print(f"Área: {figura.area():.2f}, Perímetro: {figura.perimetro():.2f}")
figuras = [Circulo(3), Rectangulo(2, 5), Circulo(7)]
for f in figuras:
imprimir_info(f)
Varios métodos abstractos
Puedes declarar tantos métodos abstractos como necesites. Una clase puede incluso heredar de múltiples ABCs, acumulando todos sus contratos:
from abc import ABC, abstractmethod
class Serializable(ABC):
@abstractmethod
def to_dict(self) -> dict:
...
@abstractmethod
def to_json(self) -> str:
...
class Validable(ABC):
@abstractmethod
def es_valido(self) -> bool:
...
class Producto(FiguraGeometrica, Serializable, Validable):
# Debe implementar: area(), perimetro(), to_dict(), to_json(), es_valido()
...
Si olvidas alguno, Python te lo dice en el momento de instanciar, con la lista completa de los métodos pendientes. Es como tener un compilador de tipos de forma completamente dinámica.
Métodos concretos en una ABC
Una clase abstracta puede contener métodos perfectamente implementados. De hecho, este es uno de sus superpoderes respecto a las interfaces puras de otros lenguajes: puedes definir comportamiento compartido que todas las subclases heredan automáticamente.
class Animal(ABC):
def __init__(self, nombre: str):
self.nombre = nombre
@abstractmethod
def hablar(self) -> str:
...
# Método concreto: todas las subclases lo heredan
def presentarse(self) -> str:
return f"Soy {self.nombre} y digo: {self.hablar()}"
class Perro(Animal):
def hablar(self) -> str:
return "¡Guau!"
class Gato(Animal):
def hablar(self) -> str:
return "¡Miau!"
p = Perro("Rex")
print(p.presentarse()) # Soy Rex y digo: ¡Guau!
g = Gato("Misi")
print(g.presentarse()) # Soy Misi y digo: ¡Miau!
Fíjate en el patrón: presentarse() llama a hablar(), que es abstracto. El método concreto delega en el abstracto. Este es el núcleo del patrón Template Method: defines el esqueleto del algoritmo en la clase base y dejas que las subclases rellenen los pasos específicos.
@property abstracta
Puedes combinar @property con @abstractmethod para exigir que las subclases expongan un atributo como propiedad. El orden de los decoradores importa: primero @property, luego @abstractmethod.
from abc import ABC, abstractmethod
class Vehiculo(ABC):
@property
@abstractmethod
def velocidad_maxima(self) -> int:
"""Velocidad máxima en km/h."""
...
@property
@abstractmethod
def tipo_combustible(self) -> str:
...
class Coche(Vehiculo):
@property
def velocidad_maxima(self) -> int:
return 220
@property
def tipo_combustible(self) -> str:
return "gasolina"
c = Coche()
print(c.velocidad_maxima) # 220
print(c.tipo_combustible) # gasolina
Esto es útil cuando quieres que ciertos datos sean obligatoriamente accesibles como atributos (sin paréntesis), no como llamadas a método.
Verificar la jerarquía
Las funciones isinstance() e issubclass() funcionan con ABCs exactamente igual que con clases normales:
c = Circulo(5)
isinstance(c, Circulo) # True
isinstance(c, FiguraGeometrica) # True — es un Circulo, pero TAMBIÉN una FiguraGeometrica
issubclass(Circulo, FiguraGeometrica) # True
# Puedes ver qué métodos abstractos tiene pendientes una clase
print(FiguraGeometrica.__abstractmethods__)
# frozenset({'area', 'perimetro'})
El atributo __abstractmethods__ es un frozenset con los nombres de los métodos aún no implementados. Si la subclase los implementa todos, el frozenset queda vacío y la clase es instanciable.
También existe el método register() de ABCMeta, que permite registrar una clase como "virtual subclass" sin herencia real. Es útil para integrar código heredado o de terceros:
# Una clase que NO hereda de FiguraGeometrica pero queremos que pase isinstance
class FiguraExterna:
def area(self): return 0.0
def perimetro(self): return 0.0
FiguraGeometrica.register(FiguraExterna)
f = FiguraExterna()
print(isinstance(f, FiguraGeometrica)) # True
Ojo: register() no verifica que la clase realmente implemente los métodos. Es un contrato de honor, no forzado.
collections.abc
La biblioteca estándar de Python incluye un módulo lleno de clases abstractas predefinidas para tipos de datos: collections.abc. Cuando creas una colección personalizada, heredar de ellas es la forma correcta de hacerlo:
from collections.abc import Sequence
class MiLista(Sequence):
def __init__(self, datos):
self._datos = list(datos)
def __getitem__(self, index):
return self._datos[index]
def __len__(self):
return len(self._datos)
# ¡Ya tienes gratis: __contains__, __iter__, __reversed__, index, count!
ml = MiLista([3, 1, 4, 1, 5])
print(len(ml)) # 5
print(3 in ml) # True
print(ml.count(1)) # 2
Implementando solo dos métodos abstractos (__getitem__ y __len__), obtienes 5 métodos concretos de regalo. Las ABCs de collections.abc no son solo contratos: son mixins con implementaciones inteligentes.
Las más útiles: Sequence, MutableSequence, Mapping, MutableMapping, Set, Iterable, Iterator, Callable.
¿Cuándo usarlas?
Las clases abstractas son la herramienta adecuada cuando:
- Diseñas un framework o biblioteca que otros van a extender. Defines la arquitectura, ellos ponen la carne.
- Necesitas polimorfismo garantizado: varias clases que se usan de forma intercambiable y que deben tener la misma interfaz.
- Quieres compartir lógica común entre subclases (métodos concretos) sin dejar de exigir implementación de los pasos específicos (Template Method).
- El error de "olvidé implementar un método" tiene consecuencias graves en producción y quieres detectarlo lo antes posible.
Cuándo no usarlas: si solo necesitas un "contrato de comportamiento" sin herencia, considera los Protocol de typing (introducidos en Python 3.8). Los Protocols permiten duck typing estructural sin herencia explícita. La diferencia clave: ABC fuerza herencia; Protocol no.
from typing import Protocol
class Serializable(Protocol):
def to_dict(self) -> dict: ...
# Cualquier clase con to_dict() pasa el check, sin heredar de nada
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre Clases Abstractas en Python
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Clases Abstractas en Python? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!