W3docs

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 abcABC y abstractmethod
  • 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 time

La 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 area

La 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:

NombreQué es
ABCUna clase base auxiliar. Héreda de ella para hacer abstracta tu clase.
abstractmethodUn decorador que marca un método como abstracto.

Impórtalos así:

from abc import ABC, abstractmethod

Crear 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):
        pass

Shape 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, perimeter

Implementar 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())            # 20

Tanto 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 perimeter

El 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: Meow

La 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.2743

Este 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 $100

Agregar 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.

Práctica

Práctica
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
Was this page helpful?