Chapter 35, Intermediate
Closures and decorators
A decorator is a function that takes a function and returns another, with extra behavior. To write a decorator without errors, it is necessary to understand closures and `functools.wraps`.
Code for this chapter: intermediario/cap35_decoradores.py
Closures
A function defined inside another "remembers" the variables of the outer function, even after it has finished. This is called closure. The keyword nonlocal allows you to modify the variable of the outer function:
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
Each call of criar_contador creates a total itself. It is a lightweight way to store state without writing a class.
The simplest decorator, and the mistake almost everyone makes
A decorator replaces the function with a wrapper. If the wrapper does not accept the arguments of the original function, the decorated function stops working:
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
The correct form has three precautions: the wrapper accepts *args and **kwargs, returns the result of the original function and uses functools.wraps to preserve the name and documentation:
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.
Without the wraps, multiplicar.__name__ it would be "wrapper", which hinders debugging, logging, and documentation tools.
Decorators with parameters
To receive arguments (@repetir(3)), the decorator becomes a factory of decorators: a function that returns the real decorator. There are three levels of function:
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á
A useful example: try again
Network calls fail from time to time. A retry decorator separates this policy from the business code:
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
In production, I would add an exponential backoff between attempts (backoff) and a timeout. The architecture chapter shows a version that injects the wait function to be able to test without actually waiting.
Ready cache
The standard library already includes useful decorators. The functools.cache caches the result of each call and transforms an exponential recursion into 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 only for pure functions
The
cacheassumes that the same input always gives the same output. Using it on functions that read the clock, the database, or the network returns stale answers. And it keeps the arguments forever, so don't use it with unlimited inputs.
Exercise 1
Count the calls of a function
Write the decorator contar_chamadas that keeps the attribute chamadas in the decorated function, without losing the original name.