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:

  1. 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.
  2. 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_checkable verifica 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)