Classes abstratas (ABC)
Esta aula explora classes abstratas em Python usando o módulo abc, abordando a criação de métodos abstratos, a diferença entre classes abstratas e interfaces, e uma visão geral dos Protocolos (PEP 544). Você aprenderá a projetar hierarquias flexíveis e a impor contratos de implementação, com exemplos práticos e exercícios.
Nesta aula, você vai aprender a usar o módulo abc para criar classes abstratas em Python. Classes abstratas são fundamentais para o design orientado a objetos, pois permitem definir uma interface comum e garantir que subclasses implementem certos métodos. Isso torna o código mais robusto e expressivo, especialmente em projetos grandes.
Além disso, vamos explorar o conceito de interfaces em Python, que não possui uma palavra-chave própria como em Java, mas pode ser implementada via classes abstratas. Também daremos uma olhada nos Protocolos (PEP 544), que oferecem uma alternativa para verificação estrutural de tipos. Ao final, você terá uma compreensão sólida de como e quando usar cada abordagem.
O módulo abc
O módulo abc (Abstract Base Classes) fornece as ferramentas para definir classes abstratas em Python. A classe ABC é a base para criar classes abstratas, e o decorador @abstractmethod marca métodos que devem ser implementados pelas subclasses.
Uma classe abstrata não pode ser instanciada diretamente; ela serve como um contrato para as subclasses. Se uma subclasse não implementar todos os métodos abstratos, ela também se torna abstrata e não pode ser instanciada. Isso garante que todos os objetos concretos tenham a funcionalidade esperada.
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def fazer_som(self):
pass
@abstractmethod
def mover(self):
pass
# Tentativa de instanciar uma classe abstrata gera erro:
# animal = Animal() # TypeError: Can't instantiate abstract class Animal with abstract methods mover, fazer_som
class Cachorro(Animal):
def fazer_som(self):
return "Au au!"
def mover(self):
return "Correndo"
c = Cachorro()
print(c.fazer_som()) # Au au!
O módulo abc também oferece outros utilitários, como @abstractproperty (depreciado em favor de @property combinado com @abstractmethod) e ABCMeta, que é a metaclasse usada por ABC. Você raramente precisará usar ABCMeta diretamente, mas é útil saber que ele controla a verificação de métodos abstratos.
Métodos abstratos
Um método abstrato é um método que tem declaração, mas não tem implementação (ou tem uma implementação que deve ser sobrescrita). Em Python, usamos o decorador @abstractmethod para marcá-lo. A implementação real fica nas subclasses.
Além de métodos comuns, você pode definir propriedades abstratas e métodos de classe abstratos. Isso permite criar contratos mais ricos. Por exemplo, uma propriedade abstrata força que toda subclasse defina um atributo com aquele nome.
from abc import ABC, abstractmethod
class Forma(ABC):
@property
@abstractmethod
def area(self):
"""Retorna a área da forma."""
pass
@classmethod
@abstractmethod
def criar_aleatorio(cls):
"""Cria uma instância com parâmetros aleatórios."""
pass
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
@property
def area(self):
return 3.14159 * self.raio ** 2
@classmethod
def criar_aleatorio(cls):
import random
return cls(random.random() * 10)
c = Circulo.criar_aleatorio()
print(c.area) # Valor da área
No exemplo acima, area é uma propriedade abstrata e criar_aleatorio é um método de classe abstrato. Subclasses precisam implementar ambos. Isso é útil para garantir uma API consistente.
Interfaces em Python
Python não tem uma palavra-chave interface como Java ou C#. Em vez disso, classes abstratas podem ser usadas para simular interfaces, especialmente quando você quer definir apenas métodos abstratos, sem implementação concreta. Nesse caso, a classe abstrata atua como uma interface pura.
No entanto, é importante notar que Python adota o duck typing: se um objeto tem os métodos esperados, ele pode ser usado, independentemente de sua herança. Portanto, as interfaces em Python são mais sobre documentação e verificação explícita do que sobre imposição rígida.
from abc import ABC, abstractmethod
class InterfaceRepositorio(ABC):
@abstractmethod
def salvar(self, entidade):
"""Salva a entidade."""
pass
@abstractmethod
def buscar(self, id):
"""Busca uma entidade pelo id."""
pass
class RepositorioUsuario(InterfaceRepositorio):
def salvar(self, usuario):
print(f"Salvando usuário {usuario.nome} no banco.")
def buscar(self, id):
print(f"Buscando usuário com id {id}")
return {"id": id, "nome": "João"}
# Uso
repo = RepositorioUsuario()
repo.salvar(usuario) # Supondo que usuario exista
Nesse exemplo, InterfaceRepositorio define o contrato que qualquer repositório deve seguir. Subclasses concretas implementam os métodos. Essa abordagem facilita a troca de implementações e testes com mocks.
Protocolos (visão geral)
Protocolos, introduzidos no Python 3.8 (PEP 544), oferecem uma alternativa para verificação estrutural de tipos. Enquanto classes abstratas exigem herança explícita, os protocolos permitem que você defina uma interface que qualquer classe pode satisfazer, mesmo sem herdar dela. Isso é útil para tipagem estática e duck typing.
Um protocolo é definido com a classe typing.Protocol e usa métodos normais (sem @abstractmethod). A verificação é feita em tempo de análise estática (por exemplo, com mypy) e, opcionalmente, em tempo de execução com @runtime_checkable.
from typing import Protocol
class PodeFalar(Protocol):
def falar(self) -> str:
...
class Pessoa:
def falar(self) -> str:
return "Oi!"
class Robo:
def falar(self) -> str:
return "Beep boop"
def cumprimentar(entidade: PodeFalar) -> None:
print(entidade.falar())
cumprimentar(Pessoa()) # Funciona
cumprimentar(Robo()) # Funciona
No exemplo, Pessoa e Robo não herdam de PodeFalar, mas possuem o método falar com a mesma assinatura, então são estruturalmente compatíveis. Em tempo de execução, sem @runtime_checkable, a função cumprimentar aceitaria qualquer objeto; a verificação seria apenas estática. Para verificação em tempo de execução, use o decorador:
from typing import Protocol, runtime_checkable
@runtime_checkable
class PodeFalar(Protocol):
def falar(self) -> str:
...
print(isinstance(Pessoa(), PodeFalar)) # True
Protocolos são particularmente úteis para bibliotecas e APIs, onde você quer aceitar qualquer objeto que implemente os métodos necessários, sem forçar herança. Eles complementam as classes abstratas: use classes abstratas quando você precisa de herança e reuso de código, e protocolos quando você quer apenas definir uma interface estrutural.
Boas práticas
- Use classes abstratas para hierarquias que compartilham implementação e estado. Não as use apenas para impor métodos, se você não precisa de herança.
- Prefira protocolos para contratos estruturais, especialmente em funções que aceitam múltiplos tipos.
- Documente o contrato com docstrings nos métodos abstratos.
- Evite classes abstratas com muitos métodos; isso pode indicar que a abstração não está bem definida.
- Use
@abstractmethodem combinação com@propertyou@classmethodpara forçar implementação de características especiais.
Referências
- Documentação oficial do módulo abc
- Glossário Python: Abstract Base Class
- PEP 3119 – Introducing Abstract Base Classes
- PEP 544 – Protocols: Structural subtyping
- Documentação do typing.Protocol
- Mypy: Protocol types
Exercícios
- Crie uma classe abstrata
Veiculocom métodos abstratosacelerar()efrear(). Implemente duas subclasses:CarroeBicicleta. NoCarro,acelerar()imprime "Acelerando carro" efrear()imprime "Freando carro". NaBicicleta,acelerar()imprime "Pedalando" efrear()imprime "Apertando freio". Teste instanciando cada um.✓ Resposta:from abc import ABC, abstractmethod class Veiculo(ABC): @abstractmethod def acelerar(self): pass @abstractmethod def frear(self): pass class Carro(Veiculo): def acelerar(self): print("Acelerando carro") def frear(self): print("Freando carro") class Bicicleta(Veiculo): def acelerar(self): print("Pedalando") def frear(self): print("Apertando freio") # Teste carro = Carro() carro.acelerar() carro.frear() bike = Bicicleta() bike.acelerar() bike.frear() - Defina uma interface
FormaGeometrica(classe abstrata) com métodosarea()eperimetro(). ImplementeQuadradoeCirculo. Crie uma funçãoexibir_dados(forma)que recebe um objeto com essa interface e imprime a área e o perímetro. Teste com ambas as classes.✓ Resposta:from abc import ABC, abstractmethod import math class FormaGeometrica(ABC): @abstractmethod def area(self): pass @abstractmethod def perimetro(self): pass class Quadrado(FormaGeometrica): def __init__(self, lado): self.lado = lado def area(self): return self.lado ** 2 def perimetro(self): return 4 * self.lado class Circulo(FormaGeometrica): def __init__(self, raio): self.raio = raio def area(self): return math.pi * self.raio ** 2 def perimetro(self): return 2 * math.pi * self.raio def exibir_dados(forma): print(f"Área: {forma.area():.2f}") print(f"Perímetro: {forma.perimetro():.2f}") # Teste q = Quadrado(5) c = Circulo(3) exibir_dados(q) exibir_dados(c) - Explique a diferença entre classes abstratas e protocolos em Python. Dê um exemplo onde cada um seria mais adequado.✓ Resposta: Classes abstratas são usadas quando você quer definir uma hierarquia de classes com implementação compartilhada e forçar subclasses a implementar métodos. Elas exigem herança explícita. Protocolos são usados para definir uma interface estrutural: qualquer classe que tenha os métodos necessários pode ser considerada como implementando o protocolo, sem precisar herdar. Classes abstratas são mais adequadas quando há código comum para reutilizar (ex: método que já tem implementação base). Protocolos são mais adequados para bibliotecas e funções que aceitam qualquer objeto com determinados métodos, como em duck typing. Exemplo:
Animalcomfazer_sompode ser uma classe abstrata paraCachorroeGato; um protocoloVoadorseria adequado para aceitar qualquer objeto com métodovoar(), comoPassaroeAviao. - Crie um protocolo chamado
Imprimivelcom um métodoimprimir()que recebe uma string e a imprime. Implemente duas classes que satisfaçam esse protocolo sem herdar dele:ConsoleeArquivo. Use@runtime_checkablee verifique comisinstanceque objetos dessas classes são instâncias do protocolo.✓ Resposta:from typing import Protocol, runtime_checkable @runtime_checkable class Imprimivel(Protocol): def imprimir(self, texto: str) -> None: ... class Console: def imprimir(self, texto: str) -> None: print(texto) class Arquivo: def __init__(self, nome_arquivo): self.nome_arquivo = nome_arquivo def imprimir(self, texto: str) -> None: with open(self.nome_arquivo, 'w') as f: f.write(texto) # Teste c = Console() a = Arquivo("saida.txt") print(isinstance(c, Imprimivel)) # True print(isinstance(a, Imprimivel)) # True - Usando
ABC, crie uma classe abstrataFuncionariocom um atributonome, um método abstratocalcular_salario()e um método concretoexibir_informacoes()que imprime o nome e o salário. ImplementeFuncionarioHorista(salário por horas) eFuncionarioAssalariado(salário fixo). Instancie e chameexibir_informacoes()para cada.✓ Resposta:from abc import ABC, abstractmethod class Funcionario(ABC): def __init__(self, nome): self.nome = nome @abstractmethod def calcular_salario(self): pass def exibir_informacoes(self): print(f"Nome: {self.nome}, Salário: {self.calcular_salario():.2f}") class FuncionarioHorista(Funcionario): def __init__(self, nome, horas_trabalhadas, valor_hora): super().__init__(nome) self.horas_trabalhadas = horas_trabalhadas self.valor_hora = valor_hora def calcular_salario(self): return self.horas_trabalhadas * self.valor_hora class FuncionarioAssalariado(Funcionario): def __init__(self, nome, salario_mensal): super().__init__(nome) self.salario_mensal = salario_mensal def calcular_salario(self): return self.salario_mensal # Teste f1 = FuncionarioHorista("Ana", 160, 50) f2 = FuncionarioAssalariado("Bruno", 8000) f1.exibir_informacoes() f2.exibir_informacoes()
Observações finais
Classes abstratas e protocolos são ferramentas poderosas para criar código flexível e bem estruturado. Lembre-se de que Python valoriza a simplicidade e o duck typing; use essas ferramentas quando elas agregarem clareza e segurança ao seu projeto, sem exagerar na complexidade. Pratique os exercícios e experimente combinar classes abstratas com protocolos para entender melhor as diferenças e quando cada uma é mais apropriada.