O que são classes abstratas (ABCs) e por que elas existem
Em projetos pequenos, muitas vezes basta “funcionar”: se um objeto tem os métodos esperados, você usa (duck typing). Em projetos maiores, com múltiplas equipes, módulos e integrações, é comum precisar de um contrato formal: uma definição explícita do que uma implementação deve oferecer. Em Python, esse contrato pode ser expresso com classes abstratas (ABCs, de Abstract Base Classes), usando o módulo abc.
Uma ABC permite:
- Definir uma API mínima obrigatória (métodos/atributos que precisam existir).
- Falhar cedo: instanciar uma classe incompleta gera erro claro.
- Guiar implementações e facilitar refatorações, porque o contrato fica centralizado.
- Melhorar a legibilidade: ao ler a ABC, você entende o “papel” (interface) esperado.
Duck typing vs ABC: quando usar cada um
| Cenário | Preferir | Motivo |
|---|---|---|
| Código interno simples, poucas implementações, baixo risco | Duck typing | Menos cerimônia, mais flexibilidade |
| Biblioteca/SDK, plugin system, integrações externas | ABCs | Contrato explícito e erros antecipados |
| Equipe grande, várias implementações do mesmo papel | ABCs | Padronização e manutenção |
| Você quer aceitar “qualquer coisa” que tenha certos métodos | Duck typing | Interoperabilidade sem herança |
Um ponto importante: ABC não elimina duck typing. Você pode usar ABC para documentar e validar internamente, e ainda aceitar objetos “compatíveis” em pontos específicos. A diferença é que a ABC torna o contrato explícito.
Ferramentas do módulo abc: ABC e abstractmethod
O módulo abc fornece:
ABC: classe base para criar uma ABC.@abstractmethod: decorador que marca métodos obrigatórios.
Regras práticas:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Uma classe que herda de uma ABC e não implementa todos os métodos abstratos não pode ser instanciada.
- Métodos abstratos podem ter corpo (por exemplo, para compartilhar lógica), mas continuam obrigatórios.
- Você pode combinar métodos abstratos com métodos concretos auxiliares na ABC.
Exemplo prático: contratos para Pagamento, Repositório e Exportação
Vamos montar três “interfaces” (ABCs) comuns em sistemas reais:
- GatewayDePagamento: processa pagamentos e estorna.
- RepositorioDeDados: salva e busca entidades.
- Exportador: exporta dados em um formato.
Passo 1: definir tipos de domínio simples (DTOs) e exceções
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol, Iterable
@dataclass(frozen=True)
class Pagamento:
id: str
valor: Decimal
moeda: str
@dataclass(frozen=True)
class Recibo:
pagamento_id: str
status: str # ex.: "aprovado", "recusado"
mensagem: str
class ContratoNaoAtendidoError(TypeError):
"""Erro de contrato para deixar falhas mais claras em tempo de execução."""
class PagamentoRecusadoError(RuntimeError):
pass
Observação: ContratoNaoAtendidoError é opcional, mas útil para padronizar mensagens quando você validar contratos manualmente em pontos críticos.
Passo 2: criar as ABCs (contratos formais)
from abc import ABC, abstractmethod
from typing import Optional
class GatewayDePagamento(ABC):
@abstractmethod
def cobrar(self, pagamento: Pagamento) -> Recibo:
"""Processa a cobrança e retorna um recibo."""
@abstractmethod
def estornar(self, pagamento_id: str) -> None:
"""Solicita estorno de um pagamento já processado."""
class RepositorioDeDados(ABC):
@abstractmethod
def salvar_pagamento(self, pagamento: Pagamento) -> None:
"""Persiste o pagamento."""
@abstractmethod
def obter_pagamento(self, pagamento_id: str) -> Optional[Pagamento]:
"""Busca um pagamento por id. Retorna None se não existir."""
class Exportador(ABC):
@abstractmethod
def exportar(self, pagamentos: list[Pagamento]) -> str:
"""Exporta pagamentos e retorna o conteúdo gerado (texto)."""
Essas ABCs são o “centro” do design orientado a interface: o restante do sistema depende delas, não de implementações concretas.
Passo 3: implementar múltiplas classes concretas
Agora criamos implementações diferentes para cada contrato.
GatewayDePagamento: implementação fake (para testes) e uma implementação “simulada”
class GatewayFake(GatewayDePagamento):
def __init__(self, aprovar: bool = True):
self._aprovar = aprovar
self._estornos: set[str] = set()
def cobrar(self, pagamento: Pagamento) -> Recibo:
if not self._aprovar:
return Recibo(pagamento_id=pagamento.id, status="recusado", mensagem="Pagamento recusado")
return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="OK")
def estornar(self, pagamento_id: str) -> None:
self._estornos.add(pagamento_id)
class GatewaySimulado(GatewayDePagamento):
def cobrar(self, pagamento: Pagamento) -> Recibo:
# Simula uma regra: valores acima de 1000 são recusados
if pagamento.valor > Decimal("1000"):
return Recibo(pagamento_id=pagamento.id, status="recusado", mensagem="Limite excedido")
return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="Aprovado")
def estornar(self, pagamento_id: str) -> None:
# Em um gateway real, aqui haveria chamada externa
return None
RepositorioDeDados: memória e “arquivo” (simples)
import json
from pathlib import Path
class RepositorioEmMemoria(RepositorioDeDados):
def __init__(self):
self._db: dict[str, Pagamento] = {}
def salvar_pagamento(self, pagamento: Pagamento) -> None:
self._db[pagamento.id] = pagamento
def obter_pagamento(self, pagamento_id: str):
return self._db.get(pagamento_id)
class RepositorioEmArquivoJSON(RepositorioDeDados):
def __init__(self, caminho: Path):
self._caminho = caminho
if not self._caminho.exists():
self._caminho.write_text(json.dumps({}), encoding="utf-8")
def _ler(self) -> dict:
return json.loads(self._caminho.read_text(encoding="utf-8"))
def _escrever(self, data: dict) -> None:
self._caminho.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def salvar_pagamento(self, pagamento: Pagamento) -> None:
data = self._ler()
data[pagamento.id] = {"id": pagamento.id, "valor": str(pagamento.valor), "moeda": pagamento.moeda}
self._escrever(data)
def obter_pagamento(self, pagamento_id: str):
data = self._ler()
item = data.get(pagamento_id)
if not item:
return None
return Pagamento(id=item["id"], valor=Decimal(item["valor"]), moeda=item["moeda"])
Exportador: CSV e JSON
class ExportadorCSV(Exportador):
def exportar(self, pagamentos: list[Pagamento]) -> str:
linhas = ["id,valor,moeda"]
for p in pagamentos:
linhas.append(f"{p.id},{p.valor},{p.moeda}")
return "\n".join(linhas)
class ExportadorJSON(Exportador):
def exportar(self, pagamentos: list[Pagamento]) -> str:
data = [
{"id": p.id, "valor": str(p.valor), "moeda": p.moeda}
for p in pagamentos
]
return json.dumps(data, ensure_ascii=False, indent=2)
Passo a passo: compondo o sistema a partir dos contratos
Agora criamos um serviço que depende apenas das interfaces (ABCs). Isso facilita trocar implementações sem alterar a regra de negócio.
class ServicoDePagamentos:
def __init__(
self,
gateway: GatewayDePagamento,
repo: RepositorioDeDados,
exportador: Exportador,
):
self._gateway = gateway
self._repo = repo
self._exportador = exportador
def processar(self, pagamento: Pagamento) -> Recibo:
recibo = self._gateway.cobrar(pagamento)
if recibo.status != "aprovado":
raise PagamentoRecusadoError(recibo.mensagem)
self._repo.salvar_pagamento(pagamento)
return recibo
def exportar_pagamentos(self, ids: list[str]) -> str:
pagamentos: list[Pagamento] = []
for pid in ids:
p = self._repo.obter_pagamento(pid)
if p is not None:
pagamentos.append(p)
return self._exportador.exportar(pagamentos)
Uso com diferentes combinações:
from decimal import Decimal
servico = ServicoDePagamentos(
gateway=GatewaySimulado(),
repo=RepositorioEmMemoria(),
exportador=ExportadorCSV(),
)
p1 = Pagamento(id="p-001", valor=Decimal("10.50"), moeda="BRL")
recibo = servico.processar(p1)
saida = servico.exportar_pagamentos(["p-001", "p-999"])
print(saida)
Erros claros: como o abc ajuda a falhar cedo
Se alguém criar uma implementação incompleta, o Python impede a instanciação e aponta quais métodos faltam.
class GatewayIncompleto(GatewayDePagamento):
def cobrar(self, pagamento: Pagamento) -> Recibo:
return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="OK")
# Falta implementar estornar
g = GatewayIncompleto() # TypeError: Can't instantiate abstract class ... with abstract method estornar
Esse tipo de falha é valioso em projetos grandes: o erro aparece no momento em que a classe é usada (instanciada), e não em um ponto distante do sistema.
Validação adicional de contrato (opcional) para mensagens ainda melhores
ABCs já fornecem validação, mas às vezes você quer validar objetos recebidos dinamicamente (por exemplo, plugins carregados em runtime). Você pode checar se um objeto é instância de uma ABC e lançar um erro padronizado.
def exigir_gateway(obj: object) -> GatewayDePagamento:
if not isinstance(obj, GatewayDePagamento):
raise ContratoNaoAtendidoError(
f"Objeto {type(obj).__name__} não atende ao contrato GatewayDePagamento"
)
return obj
Isso é útil quando o objeto vem de fora do seu controle (configuração, import dinâmico, etc.).
Design orientado a interface: boas práticas ao definir contratos
- Contratos pequenos e coesos: evite ABCs “gigantes”. Se um papel tem responsabilidades demais, divida em interfaces menores.
- Nomes de métodos orientados ao domínio: contratos são mais fáceis de entender quando refletem o vocabulário do sistema.
- Evite vazar detalhes de infraestrutura: a ABC deve descrever o que é necessário, não como é feito (ex.: não obrigue “usar requests”).
- Retornos e erros previsíveis: documente (em docstrings e tipos) o que retorna e quais exceções podem ocorrer.
- Dependa de interfaces, não de classes concretas: serviços recebem
GatewayDePagamento, nãoGatewaySimulado.
Exercícios (com foco em validar o contrato e gerar erros claros)
1) Implementação incompleta (erro obrigatório)
Crie uma classe ExportadorXML que herda de Exportador, mas propositalmente não implemente exportar. Tente instanciar e observe o erro. Depois implemente exportar retornando uma string XML simples.
2) Contrato com método abstrato + método concreto
Altere Exportador para incluir um método concreto exportar_para_arquivo(caminho: Path, pagamentos: list[Pagamento]) que chama exportar e grava o conteúdo. Mantenha exportar como abstrato. Teste com ExportadorCSV e ExportadorJSON.
3) Plugin carregado dinamicamente (validação manual)
Simule um “plugin” vindo de fora: crie uma classe CoisaQualquer com um método cobrar, mas sem estornar. Passe uma instância para exigir_gateway e garanta que o erro lançado seja ContratoNaoAtendidoError com uma mensagem clara.
4) Repositório alternativo com regra de negócio
Implemente RepositorioSomenteLeitura que herda de RepositorioDeDados. Faça salvar_pagamento lançar PermissionError. Use no ServicoDePagamentos e observe onde o erro ocorre. Em seguida, ajuste o serviço para tratar esse caso e retornar uma mensagem de falha adequada (sem mudar o contrato).
5) Teste de contrato com múltiplas implementações
Escreva uma função teste_gateway(gateway: GatewayDePagamento) que:
- Cria um
Pagamentode baixo valor e verifica quecobrarretorna umRecibocompagamento_idcorreto. - Chama
estornare verifica que não lança exceção.
Execute esse teste com GatewayFake e GatewaySimulado. A ideia é garantir que todas as implementações respeitam o mesmo contrato e comportamento mínimo esperado.