Clases base abstractas de Python (ABC)
Aprende cómo las clases base abstractas de Python (ABC) imponen una interfaz común entre subclases usando @abstractmethod, propiedades abstractas y el módulo abc.
Una clase abstracta es una clase que no puede instanciarse por sí sola. Existe únicamente para definir una interfaz compartida — un conjunto de métodos que cada subclase concreta debe implementar. Python ofrece esto a través del módulo integrado abc (Abstract Base Classes).
Este capítulo cubre:
- Por qué existen las clases abstractas y cuándo usarlas
- El módulo
abc—ABCyabstractmethod - Cómo imponer contratos de métodos con
@abstractmethod - Propiedades abstractas
- Métodos concretos dentro de una clase abstracta
- Clases abstractas vs. lanzar
NotImplementedError - Un ejemplo del mundo real
Antes de leer este capítulo, asegúrate de estar familiarizado con clases y objetos de Python y con la herencia.
¿Por qué usar clases abstractas?
A medida que agregas más subclases a una jerarquía, es fácil olvidar implementar un método requerido. El problema solo aparece en tiempo de ejecución, dentro de tu código, lo que hace que depurar sea doloroso.
Las clases base abstractas resuelven esto moviendo el error al momento en que se instancia una clase — no al momento en que se llama a un método inexistente. Actúan como un contrato: cada subclase concreta debe implementar cada método abstracto, o Python se negará a crear un objeto a partir de ella.
Clases abstractas vs. lanzar NotImplementedError
Antes de que existiera el módulo abc, el patrón habitual era lanzar NotImplementedError en un método de la clase base:
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
pass # forgot to implement area()
c = Circle() # No error yet — Python lets this through
c.area() # NotImplementedError only here, at call timeLa debilidad: Python crea el objeto sin problema. El error es invisible hasta que el método es realmente llamado, lo cual puede ocurrir en una parte completamente diferente del programa.
Con una clase abstracta, el error ocurre de inmediato:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
pass # still missing area()
c = Circle()
# TypeError: Can't instantiate abstract class Circle
# with abstract method areaLa clase falla en el punto de creación — exactamente donde se violó el contrato.
El módulo abc
El módulo abc de Python proporciona dos herramientas que usarás constantemente:
| Nombre | Qué es |
|---|---|
ABC | Una clase base auxiliar. Héreda de ella para hacer abstracta tu clase. |
abstractmethod | Un decorador que marca un método como abstracto. |
Impórtalos así:
from abc import ABC, abstractmethodCrear una clase abstracta
Hereda de ABC y decora al menos un método con @abstractmethod:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
passShape ahora declara que cualquier cosa que se llame a sí misma una figura debe proporcionar area() y perimeter(). No puedes crear un Shape directamente:
s = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeterImplementar una clase abstracta
Una clase concreta es aquella que hereda de la clase abstracta e implementa todos los métodos abstractos. Solo entonces puedes crear instancias:
Implementar una clase abstracta con Circle y Rectangle
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
c = Circle(5)
r = Rectangle(4, 6)
print(round(c.area(), 4)) # 78.5398
print(round(c.perimeter(), 4)) # 31.4159
print(r.area()) # 24
print(r.perimeter()) # 20Tanto Circle como Rectangle implementan todos los métodos abstractos, por lo que Python les permite ser instanciados.
¿Qué ocurre cuando falta un método?
Si una subclase implementa solo algunos métodos abstractos, sigue siendo abstracta — instanciarla lanza un TypeError que indica el método faltante:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class IncompleteShape(Shape):
def area(self):
return 0
# perimeter() not implemented
s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
# with abstract method perimeterEl mensaje de error te indica exactamente qué método olvidaste — mucho más útil que un fallo genérico en tiempo de llamada.
Propiedades abstractas
También puedes hacer abstracta una propiedad. Esto obliga a las subclases a proporcionar una propiedad (o un atributo calculado a partir de una propiedad), no simplemente un método normal. Usa @property y @abstractmethod juntos, con @property encima:
Uso de propiedades abstractas
from abc import ABC, abstractmethod
class Animal(ABC):
@property
@abstractmethod
def sound(self):
pass
def describe(self):
return f"I make the sound: {self.sound}"
class Dog(Animal):
@property
def sound(self):
return "Woof"
class Cat(Animal):
@property
def sound(self):
return "Meow"
dog = Dog()
cat = Cat()
print(dog.describe()) # I make the sound: Woof
print(cat.describe()) # I make the sound: MeowLa propiedad sound en cada subclase se comporta como un atributo de solo lectura desde la perspectiva del llamador, mientras que la clase abstracta garantiza que cada subclase concreta proporcione una.
Métodos concretos en una clase abstracta
Las clases abstractas no se limitan a métodos abstractos. Pueden contener métodos completamente implementados (concretos) que todas las subclases comparten automáticamente. Esta es la diferencia clave entre las clases abstractas y las interfaces simples en otros lenguajes — puedes incluir lógica compartida en la clase abstracta:
Compartir lógica común mediante un método concreto
from abc import ABC, abstractmethod
class Logger(ABC):
def log(self, message):
"""Concrete method — shared by all subclasses."""
formatted = self.format_message(message)
self.write(formatted)
def format_message(self, message):
return f"[LOG] {message}"
@abstractmethod
def write(self, message):
"""Abstract — each subclass decides how to output."""
pass
class ConsoleLogger(Logger):
def write(self, message):
print(message)
class FileLogger(Logger):
def __init__(self):
self.entries = []
def write(self, message):
self.entries.append(message)
console = ConsoleLogger()
console.log("Server started") # prints: [LOG] Server started
file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries) # ['[LOG] Database connected']Aquí log() y format_message() son métodos concretos compartidos. Solo write() — la parte que difiere entre los backends — es abstracto. Las subclases nunca tienen que reimplementar la lógica de formateo.
isinstance() y clases abstractas
Un objeto que es instancia de una subclase concreta también se considera instancia de la clase base abstracta. Esto te permite escribir funciones que acepten cualquier objeto que cumpla la interfaz, sin importar el tipo específico:
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
shapes = [Circle(3), Rectangle(4, 5)]
for shape in shapes:
print(isinstance(shape, Shape)) # True for both
def total_area(shapes):
return sum(s.area() for s in shapes)
print(round(total_area(shapes), 4)) # 48.2743Este es el beneficio central: total_area() funciona con cualquier Shape — pasada, presente o futura — siempre que el objeto implemente la interfaz.
Ejemplo del mundo real: procesadores de pago
Las clases abstractas destacan cuando necesitas múltiples implementaciones del mismo concepto. Considera un sistema de pago que debe soportar varios proveedores:
Clase abstracta para el procesamiento de pagos
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount):
pass
@abstractmethod
def refund(self, amount):
pass
def process(self, amount):
"""Concrete method — shared processing logic."""
print(f"Processing payment of ${amount}")
self.charge(amount)
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
print(f"Stripe: charged ${amount}")
def refund(self, amount):
print(f"Stripe: refunded ${amount}")
class PayPalProcessor(PaymentProcessor):
def charge(self, amount):
print(f"PayPal: charged ${amount}")
def refund(self, amount):
print(f"PayPal: refunded ${amount}")
processors = [StripeProcessor(), PayPalProcessor()]
for p in processors:
p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100Agregar un tercer proveedor de pago — por ejemplo BraintreeProcessor — solo requiere implementar charge() y refund(). La lógica de process() y cualquier verificación con isinstance siguen funcionando sin modificación.
Cuándo usar clases abstractas
Usa una clase abstracta cuando:
- Tienes una familia de clases relacionadas que todas deben proporcionar ciertos métodos.
- Quieres compartir algo de código (métodos concretos) mientras obligas a que otros métodos sean sobreescritos.
- Quieres que Python lance un error de inmediato si una subclase está incompleta, en lugar de esperar a un fallo en tiempo de llamada.
Probablemente no necesitas una clase abstracta cuando:
- Solo tienes una o dos subclases y es poco probable que la jerarquía crezca.
- La interfaz compartida ya está impuesta por otros medios (por ejemplo, dataclasses de Python o duck typing).
- Solo quieres evitar la instanciación directa — una solución más simple es un hook
__init_subclass__o un comentario simple.
Para conceptos estrechamente relacionados, consulta herencia de Python y polimorfismo de Python.