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:
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))
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:
from typing import Protocol
class Fechavel(Protocol):
def fechar(self) -> None: ...
class SemFechar:
pass
def encerrar(recurso: Fechavel) -> None:
recurso.fechar()
encerrar(SemFechar())
uv run mypy exemplos/tipos/protocolo_erro.py
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:
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"))
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
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)
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:
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))
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.