Pular para o conteúdo

    Capítulo 44, Avançado

    Tipagem avançada

    Protocolos, genéricos e sobrecargas deixam o verificador de tipos entender código flexível sem você abrir mão da flexibilidade do Python.

    Código deste capítulo: avancado/cap44_tipagem_avancada.py

    Protocol: tipagem estrutural

    Um Protocol descreve uma forma: qualquer classe com os métodos certos serve, sem herdar de nada. É o duck typing verificável por ferramentas. Com runtime_checkable, o isinstance também funciona, mas só checa a presença dos métodos:

    avancado/cap44_tipagem_avancada.pylinhas 10 a 34
    from typing import Protocol, runtime_checkable
    
    
    @runtime_checkable
    class Fechavel(Protocol):
        def fechar(self) -> None: ...
    
    
    class Arquivo:
        def fechar(self) -> None:
            print("arquivo fechado")
    
    
    class Conexao:
        def fechar(self) -> None:
            print("conexão fechada")
    
    
    def encerrar(recurso: Fechavel) -> None:
        recurso.fechar()
    
    
    for recurso in (Arquivo(), Conexao()):
        encerrar(recurso)
    print(isinstance(Arquivo(), Fechavel), isinstance(42, Fechavel))
    
    Saída
    arquivo fechado
    conexão fechada
    True False
    

    A vantagem sobre uma ABC é o acoplamento: Arquivo e Conexao não conhecem Fechavel. Quem define o contrato é quem consome, no lado dele, e isso torna as dependências mais fáceis de trocar e de simular em teste. Quando um objeto não cumpre o contrato, o verificador acusa:

    exemplos/tipos/protocolo_erro.py
    from typing import Protocol
    
    
    class Fechavel(Protocol):
        def fechar(self) -> None: ...
    
    
    class SemFechar:
        pass
    
    
    def encerrar(recurso: Fechavel) -> None:
        recurso.fechar()
    
    
    encerrar(SemFechar())
    
    Terminal
    uv run mypy exemplos/tipos/protocolo_erro.py
    
    Saída
    exemplos/tipos/protocolo_erro.py:16: error: Argument 1 to "encerrar" has incompatible type "SemFechar"; expected "Fechavel"  [arg-type]
    Found 1 error in 1 file (checked 1 source file)
    

    Genéricos com a sintaxe do Python 3.12

    Uma classe ou função genérica funciona com qualquer tipo e preserva a informação sobre ele. Desde o Python 3.12, a sintaxe com colchetes dispensa o TypeVar. Os parênteses depois de T: restringem os tipos aceitos:

    avancado/cap44_tipagem_avancada.pylinhas 39 a 65
    class Pilha[T]:
        def __init__(self) -> None:
            self._itens: list[T] = []
    
        def empilhar(self, item: T) -> None:
            self._itens.append(item)
    
        def desempilhar(self) -> T:
            return self._itens.pop()
    
        def __len__(self) -> int:
            return len(self._itens)
    
    
    def primeiro[T](itens: list[T]) -> T:
        return itens[0]
    
    
    def maximo[T: (int, float, str)](a: T, b: T) -> T:
        return a if a >= b else b
    
    
    pilha = Pilha[int]()
    pilha.empilhar(1)
    pilha.empilhar(2)
    print(pilha.desempilhar(), len(pilha), primeiro(["a", "b"]))
    print(maximo(3, 7), maximo("a", "b"))
    
    Saída
    2 1 a
    7 b
    

    Para o verificador, Pilha[int] só aceita inteiros, e pilha.desempilhar() é um int, sem conversão nem comentário.

    Literal, Final, TypedDict e Self

    avancado/cap44_tipagem_avancada.pylinhas 70 a 95
    from typing import Final, Literal, NotRequired, Self, TypedDict
    
    Modo = Literal["leitura", "escrita"]
    LIMITE: Final = 3
    
    
    class Opcoes(TypedDict):
        modo: Modo
        tentativas: NotRequired[int]
    
    
    def abrir(opcoes: Opcoes) -> str:
        return f"{opcoes['modo']} com {opcoes.get('tentativas', LIMITE)} tentativas"
    
    
    class Construtor:
        def __init__(self) -> None:
            self.partes: list[str] = []
    
        def adicionar(self, parte: str) -> Self:
            self.partes.append(parte)
            return self
    
    
    print(abrir({"modo": "leitura"}))
    print(Construtor().adicionar("a").adicionar("b").partes)
    
    Saída
    leitura com 3 tentativas
    ['a', 'b']
    

    O Literal restringe a valores exatos, e o Self mantém o tipo correto em métodos encadeáveis, mesmo em subclasses.

    Sobrecarga e decoradores tipados

    O @overload declara assinaturas diferentes para a mesma função, para que o verificador saiba que dobrar(4) devolve int e dobrar("ab") devolve str. E um decorador que preserva a assinatura da função decorada usa ParamSpec, que na sintaxe nova se escreve **P:

    avancado/cap44_tipagem_avancada.pylinhas 100 a 128
    import functools
    from collections.abc import Callable
    from typing import overload
    
    
    @overload
    def dobrar(x: int) -> int: ...
    @overload
    def dobrar(x: str) -> str: ...
    def dobrar(x):
        return x * 2
    
    
    def registrar[**P, R](funcao: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(funcao)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            print(f"chamando {funcao.__name__}")
            return funcao(*args, **kwargs)
    
        return wrapper
    
    
    @registrar
    def somar(a: int, b: int) -> int:
        return a + b
    
    
    print(dobrar(4), dobrar("ab"))
    print(somar(1, 2))
    
    Saída
    8 abab
    chamando somar
    3
    

    Tipagem gradual

    Você não precisa tipar tudo. Eu começo pelas fronteiras: funções públicas, modelos de dados e interfaces entre módulos. O verificador entra no CI em modo permissivo e eu aperto as regras aos poucos, módulo por módulo.

    Exercício 1

    Um repositório genérico

    Escreva Repositorio[T] com salvar(id_, item) e buscar(id_), que devolva T | None.