Capítulo 10: Polimorfismo Estrutural com typing.Protocol e Duck Typing Estático
Especialização em Sistemas com Python & POO com Type Hints • Capítulo 10
🗺️ Mapa Conceitual do Tópico
flowchart TD
subgraph TYPING_MODELS["Modelos de Tipagem em Engenharia de Software"]
NOMINAL["Subtipagem Nominal (abc.ABC / Java / C#)\nExige Declaração Explícita de Herança na Definição da Classe"]
STRUCT["Subtipagem Estrutural (typing.Protocol / Go / TS)\nConformidade Automática por Assinatura de Métodos e Atributos"]
end
subgraph PROTOCOL_FEATURES["Recursos do typing.Protocol (PEP 544)"]
CONTRACT["Contratos Declarativos com Reticências (...)\nMétodos, Argumentos Nomeados e Anotações de Retorno"]
RUN_CHECK["@runtime_checkable\nInjeção de __subclasshook__ para Suporte a isinstance()"]
GEN_PROTO["Protocolos Genéricos Parametrizados (Protocol[T])"]
end
subgraph ARCHITECTURE["Arquitetura Limpa e Inversão de Dependência"]
DIP["SOLID DIP (Dependency Inversion Principle)\nCasos de Uso Dependem de Protocolos de Portas"]
ADAPTERS["Adaptadores de Terceiros e Mocks Sem Herança Forçada"]
end
TYPING_MODELS --> PROTOCOL_FEATURES
PROTOCOL_FEATURES --> ARCHITECTURE
style TYPING_MODELS fill:#ede7f6,stroke:#512da8,stroke-width:2px
style PROTOCOL_FEATURES fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style ARCHITECTURE fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
📜 1. Subtipagem Estrutural vs Subtipagem Nominal
A engenharia de software contemporânea divide os sistemas de tipos polimórficos em duas escolas de pensamento:
- Subtipagem Nominal (Nominal Subtyping): A relação de parentesco deve ser declarada formalmente no código-fonte (
class ServicoEmail(abc.ABC):). Se a classe possuir métodos idênticos, mas não declarar explicitamente a herança, o compilador ou analisador estático rejeita a atribuição. - Subtipagem Estrutural (Structural Subtyping / Duck Typing Estático): A compatibilidade é determinada exclusivamente pela forma (shape) do objeto. Se uma classe implementa os atributos e assinaturas exigidos pelo contrato, ela é aceita de forma transparente, dispensando qualquer acoplamento de herança.
A Chegada do PEP 544 ao Python
O Python sempre foi famoso por seu Duck Typing dinâmico (“Se anda como um pato e faz quack como um pato, então é um pato”). O módulo typing.Protocol (PEP 544) elevou esse princípio ao nível da verificação estática de tipos em tempo de compilação/análise estática (mypy, ruff, PyCharm, VS Code):
from typing import Protocol
class Fechavel(Protocol):
def fechar(self) -> None:
...
class ArquivoLog:
def fechar(self) -> None:
print("Arquivo fechado.")
# Válido no mypy sem ArquivoLog herdar de Fechavel!
def liberar_recurso(recurso: Fechavel) -> None:
recurso.fechar()
🔍 2. O Decorador @runtime_checkable
Por padrão arquitetural, protocolos existem apenas no ecossistema de análise estática e não penalizam a execução na PVM. No entanto, se o seu sistema precisa validar objetos dinamicamente em tempo de execução via isinstance() ou issubclass(), a biblioteca padrão oferece o decorador @runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializavel(Protocol):
def to_dict(self) -> dict[str, object]:
...
class ClienteDTO:
def to_dict(self) -> dict[str, object]:
return {"nome": "TechCorp"}
dto = ClienteDTO()
# Avaliado com sucesso em runtime via inspeção de atributos __subclasshook__:
print(isinstance(dto, Serializavel)) # True
⚠️ Limitação Crítica em Runtime: O
@runtime_checkableverifica apenas a existência dos nomes de atributos e métodos, não inspecionando assinaturas de argumentos ou tipos de retorno em runtime. A verificação rigorosa de tipos de parâmetros continua sendo prerrogativa dos analisadores estáticos (mypy).
🏛️ 3. Protocolos Genéricos Parametrizados (Protocol[T])
Assim como em Go interfaces ou Rust traits, os protocolos em Python suportam polimorfismo paramétrico para desacoplar a camada de persistência:
from typing import Protocol, TypeVar
import uuid
T = TypeVar("T")
class Repositorio(Protocol[T]):
def obter_por_id(self, id_entidade: uuid.UUID) -> T | None:
...
def salvar(self, entidade: T) -> None:
...
def remover(self, id_entidade: uuid.UUID) -> bool:
...
A classe de repositório concreto (ex.: RepositorioPostgres, RepositorioMongo ou RepositorioEmMemoria) implementa essas operações sem jamais importar o módulo de protocolos, permitindo mockar dependências em testes unitários com facilidade absoluta.
⚖️ 4. Matriz Comparativa: abc.ABC vs typing.Protocol
| Critério Arquitetural | Classes Abstratas (abc.ABC) |
Protocolos Estruturais (typing.Protocol) |
|---|---|---|
| Mecanismo de Subtipagem | Nominal (IS-A explícito) | Estrutural (Has-A-Shape implícito) |
| Necessidade de Importar Base | Mandatória na definição da subclasse | Desnecessária (Classes de terceiros são aceitas) |
| Compartilhamento de Código | Suporta métodos concretos e estado comum | Apenas contratos vazios (...) |
| Performance em Runtime | Zero impacto | Zero impacto (exceto se usar @runtime_checkable) |
| Inversão de Dependências (DIP) | Forte acoplamento ao módulo da classe base | Desacoplamento máximo (Portas e Adaptadores Hexagonais) |