Classes Base Abstratas em Python (ABC)
Aprenda como as classes base abstratas do Python (ABC) impõem uma interface comum às subclasses usando @abstractmethod, propriedades abstratas e o módulo abc.
Uma classe abstrata é uma classe que não pode ser instanciada diretamente. Ela existe apenas para definir uma interface compartilhada — um conjunto de métodos que toda subclasse concreta deve implementar. Python fornece isso por meio do módulo embutido abc (Abstract Base Classes).
Este capítulo aborda:
- Por que as classes abstratas existem e quando usá-las
- O módulo
abc—ABCeabstractmethod - Impondo contratos de métodos com
@abstractmethod - Propriedades abstratas
- Métodos concretos dentro de uma classe abstrata
- Classes abstratas vs. lançar
NotImplementedError - Um exemplo do mundo real
Antes de ler este capítulo, certifique-se de que você está familiarizado com classes e objetos em Python e herança.
Por Que Usar Classes Abstratas?
À medida que você adiciona mais subclasses a uma hierarquia, fica fácil esquecer de implementar um método obrigatório. O problema só aparece em tempo de execução, no meio do código, o que torna a depuração dolorosa.
As classes base abstratas resolvem isso ao mover o erro para o momento em que uma classe é instanciada — não o momento em que um método ausente é chamado. Elas funcionam como um contrato: cada subclasse concreta deve implementar cada método abstrato, ou Python se recusará a criar um objeto a partir dela.
Classes abstratas vs. lançar NotImplementedError
Antes de o módulo abc existir, o padrão comum era lançar NotImplementedError em um método da classe 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 timeA fraqueza: Python cria o objeto sem reclamar. O erro fica invisível até que o método seja realmente chamado, o que pode ocorrer em uma parte completamente diferente do programa.
Com uma classe abstrata, o erro acontece imediatamente:
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 areaA classe está quebrada no ponto de criação — exatamente onde o contrato foi violado.
O Módulo abc
O módulo abc do Python fornece duas ferramentas que você usará constantemente:
| Nome | O que é |
|---|---|
ABC | Uma classe base auxiliar. Herde dela para tornar sua classe abstrata. |
abstractmethod | Um decorador que marca um método como abstrato. |
Importe-os assim:
from abc import ABC, abstractmethodCriando uma classe abstrata
Herde de ABC e decore pelo menos um método com @abstractmethod:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
passShape agora declara que qualquer coisa que se chame de forma deve fornecer area() e perimeter(). Você não pode criar um Shape diretamente:
s = Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract methods area, perimeterImplementando uma Classe Abstrata
Uma classe concreta é aquela que herda da classe abstrata e implementa todos os métodos abstratos. Somente então você pode criar instâncias:
Implementando uma classe abstrata com Circle e 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 quanto Rectangle implementam todos os métodos abstratos, então Python permite que sejam instanciados.
O que acontece quando você esquece um método?
Se uma subclasse implementa apenas alguns dos métodos abstratos, ela ainda é abstrata — instanciá-la lança um TypeError que informa qual método está faltando:
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 perimeterA mensagem de erro informa exatamente qual método você esqueceu — muito mais útil do que uma falha genérica em tempo de execução.
Propriedades Abstratas
Você também pode tornar uma propriedade abstrata. Isso força as subclasses a fornecer uma propriedade (ou atributo calculado a partir de uma propriedade), não apenas um método simples. Use @property e @abstractmethod juntos, com @property no topo:
Usando propriedades abstratas
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: MeowA propriedade sound em cada subclasse se comporta como um atributo somente leitura do ponto de vista do chamador, enquanto a classe abstrata garante que toda subclasse concreta forneça uma.
Métodos Concretos em uma Classe Abstrata
As classes abstratas não se limitam a métodos abstratos. Elas podem conter métodos completamente implementados (concretos) que todas as subclasses herdam automaticamente. Esta é a principal diferença entre classes abstratas e interfaces simples em outras linguagens — você pode colocar lógica compartilhada na classe abstrata:
Compartilhando lógica comum via 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']Aqui log() e format_message() são métodos concretos compartilhados. Apenas write() — a parte que difere entre os backends — é abstrata. As subclasses nunca precisam reimplementar a lógica de formatação.
isinstance() e Classes Abstratas
Um objeto que é uma instância de uma subclasse concreta também é considerado uma instância da classe base abstrata. Isso permite escrever funções que aceitam qualquer objeto que esteja em conformidade com a interface, sem se preocupar com o 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 é o benefício central: total_area() funciona com qualquer Shape — passado, presente ou futuro — desde que o objeto implemente a interface.
Exemplo do Mundo Real: Processadores de Pagamento
As classes abstratas se destacam quando você precisa de múltiplas implementações de um mesmo conceito. Considere um sistema de pagamento que deve suportar vários provedores:
Classe abstrata para processamento de pagamentos
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 $100Adicionar um terceiro provedor de pagamento — digamos BraintreeProcessor — requer apenas implementar charge() e refund(). A lógica de process() e quaisquer verificações com isinstance continuam funcionando sem modificação.
Quando Usar Classes Abstratas
Use uma classe abstrata quando:
- Você tem uma família de classes relacionadas que devem fornecer determinados métodos.
- Você quer compartilhar algum código (métodos concretos) enquanto força outros métodos a serem sobrescritos.
- Você quer que Python lance um erro imediatamente se uma subclasse estiver incompleta, em vez de aguardar uma falha em tempo de execução.
Você provavelmente não precisa de uma classe abstrata quando:
- Você tem apenas uma ou duas subclasses e é improvável que a hierarquia cresça.
- A interface compartilhada já é imposta por outros meios (por exemplo, dataclasses do Python ou duck typing).
- Você apenas quer impedir a instanciação direta — uma solução mais simples é um hook
__init_subclass__ou um simples comentário.
Para conceitos relacionados, veja herança em Python e polimorfismo em Python.