Capítulo 35, Intermediário
Closures e decoradores
Um decorador é uma função que recebe uma função e devolve outra, com comportamento extra. Para escrever um decorador sem erro é preciso entender closures e `functools.wraps`.
Código deste capítulo: intermediario/cap35_decoradores.py
Closures
Uma função definida dentro de outra "lembra" das variáveis da função externa, mesmo depois de ela ter terminado. Isso se chama closure. A palavra nonlocal permite alterar a variável da função externa:
def criar_contador():
total = 0
def contar():
nonlocal total
total += 1
return total
return contar
c = criar_contador()
print(c(), c(), c())
outro = criar_contador()
print(outro())
1 2 3
1
Cada chamada de criar_contador cria um total próprio. É um jeito leve de guardar estado sem escrever uma classe.
O decorador mais simples, e o erro que quase todo mundo comete
Um decorador troca a função por um wrapper. Se o wrapper não aceitar os argumentos da função original, a função decorada deixa de funcionar:
def decorador_ruim(funcao):
def wrapper():
return funcao()
return wrapper
@decorador_ruim
def soma(a, b):
return a + b
try:
soma(2, 3)
except TypeError as erro:
print(erro)
decorador_ruim.<locals>.wrapper() takes 0 positional arguments but 2 were given
A forma correta tem três cuidados: o wrapper aceita *args e **kwargs, devolve o resultado da função original e usa functools.wraps para preservar o nome e a documentação:
import functools
def registrar(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
resultado = funcao(*args, **kwargs)
print(f"{funcao.__name__}{args} -> {resultado}")
return resultado
return wrapper
@registrar
def multiplicar(a, b):
"""Multiplica dois números."""
return a * b
multiplicar(3, 4)
print(multiplicar.__name__, multiplicar.__doc__)
multiplicar(3, 4) -> 12
multiplicar Multiplica dois números.
Sem o wraps, multiplicar.__name__ seria "wrapper", o que atrapalha depuração, logs e ferramentas de documentação.
Decoradores com parâmetros
Para receber argumentos (@repetir(3)), o decorador vira uma fábrica de decoradores: uma função que devolve o decorador de verdade. São três níveis de função:
def repetir(vezes):
def decorador(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
resultado = None
for _ in range(vezes):
resultado = funcao(*args, **kwargs)
return resultado
return wrapper
return decorador
@repetir(3)
def avisar(texto):
print(texto)
avisar("olá")
olá
olá
olá
Um exemplo útil: tentar de novo
Chamadas de rede falham de vez em quando. Um decorador de tentativas separa essa política do código de negócio:
def tentar_novamente(tentativas):
def decorador(funcao):
@functools.wraps(funcao)
def wrapper(*args, **kwargs):
ultimo_erro = None
for numero in range(1, tentativas + 1):
try:
return funcao(*args, **kwargs)
except ConnectionError as erro:
ultimo_erro = erro
print(f"tentativa {numero} falhou")
raise ultimo_erro
return wrapper
return decorador
chamadas = {"total": 0}
@tentar_novamente(3)
def buscar():
chamadas["total"] += 1
if chamadas["total"] < 3:
raise ConnectionError("rede fora")
return "dados"
print(buscar())
tentativa 1 falhou
tentativa 2 falhou
dados
Em produção, eu acrescentaria uma espera crescente entre as tentativas (backoff) e um limite de tempo. O capítulo de arquitetura mostra uma versão que injeta a função de espera para poder testar sem esperar de verdade.
Cache pronto
A biblioteca padrão já traz decoradores úteis. O functools.cache guarda o resultado de cada chamada e transforma uma recursão exponencial em linear:
from functools import cache
@cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80))
print(fib.cache_info().hits > 0)
23416728348467685
True
Cache só para funções puras
O
cacheassume que a mesma entrada sempre dá a mesma saída. Usá-lo em funções que leem o relógio, o banco ou a rede devolve respostas velhas. E ele guarda os argumentos para sempre, então não o use com entradas ilimitadas.
Exercício 1
Contar as chamadas de uma função
Escreva o decorador contar_chamadas que mantenha o atributo chamadas na função decorada, sem perder o nome original.