Python Decoradores
Você já viu linhas começando com @ em cima de um def: o @property da aula @property, o @classmethod, o @dataclass. Esse @ é um decorador: uma função que recebe outra função e devolve uma versão "embrulhada" dela, com algo a mais antes ou depois. Nesta aula você vai entender como isso funciona por dentro e escrever os seus: um que anuncia, um que registra as chamadas e um que cronometra.
- Que funções são objetos: dá para guardar numa variável, passar e devolver.
- A criar uma função dentro de outra e devolvê-la.
- A escrever um decorador na mão e com a sintaxe
@, e por que as duas são a mesma coisa. - A aceitar quaisquer argumentos com
*argse**kwargse a devolver o resultado. - Por que usar
functools.wraps. - Um
@cronometrocomtime.perf_counter(), decoradores com argumentos, empilhados, e o@cachepronto. - Os erros reais:
'NoneType' object is not callable,takes 0 positional arguments,missing 1 required positional argumente o resultado que viraNone.
A ideia: o papel de presente
A Ana comprou um livro para dar de aniversário. Ela não muda o livro: embrulha. O presente continua sendo o livro, mas agora tem um papel por fora, um laço e um cartão. Quem recebe abre o embrulho, lê o cartão e encontra o livro.
O decorador faz isso com uma função. A função original (o livro) continua lá dentro, sem uma linha mudada. O decorador cria uma função nova (o embrulho) que faz alguma coisa antes, chama a original e faz alguma coisa depois. E o nome antigo passa a apontar para o embrulho.
| No presente | No Python |
|---|---|
| O livro | A função original (promocao) |
| Quem embrulha | O decorador (anunciar) |
| O pacote pronto | A função nova que o decorador devolve (embrulho) |
| O cartão e o laço | O que o embrulho faz antes e depois de chamar a original |
1. Funções são objetos
Para entender decoradores, falta uma peça: em Python, uma função é um objeto, como um número ou uma lista. O nome dela é só uma etiqueta (aula Variáveis) presa ao objeto. Sem os parênteses, você fala da função; com os parênteses, você chama a função.
def saudar(nome):
return f"Olá, {nome}!"
# 1. Uma função é um objeto: dá para guardar numa variável
cumprimentar = saudar
print(cumprimentar("Ana"))
print(saudar)
print(type(saudar))
print(saudar.__name__)
# 2. Dá para passar uma função para outra
def aplicar_em_todos(funcao, nomes):
for nome in nomes:
print(funcao(nome))
aplicar_em_todos(saudar, ["Bia", "Caio"])
$ python3 objetos.py Olá, Ana! <function saudar at 0x7f65d372f530> « o endereço muda a cada execução <class 'function'> saudar Olá, Bia! Olá, Caio!
- cumprimentar = saudar
- Sem parênteses: não chama nada. Só cola uma segunda etiqueta no mesmo objeto função.
cumprimentar("Ana")roda o mesmo código. - print(saudar)
- Mostra o objeto:
<function saudar at 0x…>, "a funçãosaudar, no endereço tal da memória". - saudar.__name__
- Toda função guarda o próprio nome no atributo
__name__. Ele vai ser importante na seção 5. - aplicar_em_todos(saudar, …)
- A função vai como argumento, sem parênteses. Lá dentro,
funcao(nome)chama. Você já fez isso com okey=dosortede com omap(aula map() e filter()).
Uma função que fabrica funções
Também dá para criar uma função dentro de outra e devolvê-la com return. A função de dentro continua lembrando as variáveis da de fora, mesmo depois que a de fora terminou:
def criar_saudacao(saudacao):
def saudar(nome):
return f"{saudacao}, {nome}!"
return saudar
bom_dia = criar_saudacao("Bom dia")
boa_noite = criar_saudacao("Boa noite")
print(bom_dia("Ana"))
print(boa_noite("Ana"))
print(bom_dia)
$ python3 fabrica.py Bom dia, Ana! Boa noite, Ana! <function criar_saudacao.<locals>.saudar at 0x7fecee73f110>
Cada chamada a criar_saudacao fabricou uma função saudar nova, cada uma com a sua saudacao guardada. O nome completo, criar_saudacao.<locals>.saudar, quer dizer "a saudar que é local de criar_saudacao". Essa função que lembra variáveis de fora tem um nome técnico: closure ("fechamento").
Com essas duas peças (receber uma função e devolver uma função nova), dá para montar um decorador.
2. O primeiro decorador, escrito na mão
A padaria tem um alto-falante. Antes de cada aviso, toca a chamada "Atenção, clientes!"; depois, "Obrigado pela atenção". Em vez de repetir isso em cada aviso, a Ana escreve um decorador:
def anunciar(funcao):
def embrulho():
print("[Atenção, clientes!]")
funcao()
print("[Obrigado pela atenção.]")
return embrulho
def promocao():
print("Pão francês pela metade do preço!")
promocao = anunciar(promocao)
promocao()
$ python3 na_mao.py [Atenção, clientes!] Pão francês pela metade do preço! [Obrigado pela atenção.]
- def anunciar(funcao):
- O decorador. Recebe a função a ser embrulhada.
- def embrulho():
- A função nova, criada lá dentro. Ela ainda não roda: só fica pronta.
- print(…) / funcao() / print(…)
- O que o embrulho vai fazer quando for chamado: o "antes", a função original, o "depois". O
funcaoé a variável de fora que o embrulho lembra. - return embrulho
- Devolve a função nova, sem parênteses: entrega o pacote, não abre.
- promocao = anunciar(promocao)
- A linha que decora: passa a
promocaooriginal para oanunciare cola a etiquetapromocaono embrulho que voltou. - promocao()
- Agora chama o embrulho, que chama a original lá dentro.
3. A sintaxe @
A linha promocao = anunciar(promocao) tem um problema: fica longe do def, e quem lê a função não sabe que ela vai ser embrulhada. O @ faz a mesma coisa, colado em cima do def:
def anunciar(funcao):
def embrulho():
print("[Atenção, clientes!]")
funcao()
print("[Obrigado pela atenção.]")
return embrulho
@anunciar
def promocao():
print("Pão francês pela metade do preço!")
@anunciar
def fechamento():
print("A padaria fecha em 10 minutos.")
promocao()
print()
fechamento()
$ python3 arroba.py [Atenção, clientes!] Pão francês pela metade do preço! [Obrigado pela atenção.] [Atenção, clientes!] A padaria fecha em 10 minutos. [Obrigado pela atenção.]
@anunciar em cima de def promocao quer dizer, exatamente: "crie a função promocao e, logo em seguida, faça promocao = anunciar(promocao)". O decorador roda uma vez, quando o Python passa pelo def; o embrulho roda a cada chamada. Dois avisos, um decorador só: o "antes" e o "depois" estão escritos num lugar.
Missão: o decorador gritar deve deixar em maiúsculas o texto que a função devolve. Faça duas mudanças: no embrulho, devolva resultado.upper() no lugar de resultado; e ponha @gritar na linha de cima do def saudar.
def gritar(funcao):
def embrulho(*args, **kwargs):
resultado = funcao(*args, **kwargs)
return resultado
return embrulho
def saudar(nome):
return f"Bom dia, {nome}!"
print(saudar("Ana"))
def gritar(funcao):
def embrulho(*args, **kwargs):
resultado = funcao(*args, **kwargs)
return resultado.upper()
return embrulho
@gritar
def saudar(nome):
return f"Bom dia, {nome}!"
print(saudar("Ana"))
O *args, **kwargs do embrulho é a próxima seção: é o que permite a saudar receber o nome.
4. Funções com argumentos e com resultado
O embrulho() do anunciar não recebe nada e não devolve nada. Para decorar qualquer função, o embrulho precisa de duas coisas:
- aceitar quaisquer argumentos e repassá-los:
*args(os posicionais, numa tupla) e**kwargs(os nomeados, num dicionário), da aula *args e **kwargs; - devolver o resultado da função original com
return.
def registrar(funcao):
def embrulho(*args, **kwargs):
print(f"Chamando {funcao.__name__} com {args} {kwargs}")
resultado = funcao(*args, **kwargs)
print(f"{funcao.__name__} devolveu {resultado}")
return resultado
return embrulho
@registrar
def somar(a, b):
return a + b
@registrar
def preco_final(preco, desconto=0):
return preco - desconto
total = somar(2, 3)
print("total =", total)
preco_final(10.0, desconto=1.5)
$ python3 argumentos.py
Chamando somar com (2, 3) {}
somar devolveu 5
total = 5
Chamando preco_final com (10.0,) {'desconto': 1.5}
preco_final devolveu 8.5
- def embrulho(*args, **kwargs):
- Aceita qualquer combinação de argumentos. Em
somar(2, 3),argsé(2, 3)ekwargsé{}; empreco_final(10.0, desconto=1.5),argsé(10.0,)ekwargsé{'desconto': 1.5}. - funcao(*args, **kwargs)
- Aqui o
*e o**fazem o contrário: desempacotam a tupla e o dicionário de volta em argumentos. A original recebe exatamente o que foi passado ao embrulho. - return resultado
- Repassa o que a original devolveu. Sem esta linha,
totalseriaNone(veja em Quando dá errado).
Este é o molde de quase todo decorador: def embrulho(*args, **kwargs):, o "antes", resultado = funcao(*args, **kwargs), o "depois", return resultado.
5. functools.wraps: o embrulho com a etiqueta certa
Há um efeito colateral. Depois de decorada, a etiqueta somar aponta para o embrulho, e o embrulho tem o nome e a documentação dele, não os da original:
def registrar(funcao):
def embrulho(*args, **kwargs):
print(f"Chamando {funcao.__name__}")
return funcao(*args, **kwargs)
return embrulho
@registrar
def somar(a, b):
"""Soma dois números."""
return a + b
print(somar.__name__)
print(somar.__doc__)
print(somar)
$ python3 nome_perdido.py embrulho None <function registrar.<locals>.embrulho at 0x7f03bf9a31c0>
O nome virou embrulho e a docstring (aula Docstrings) sumiu: None. Parece detalhe, mas quem usa o help(somar), os testes, as mensagens de erro e o logging passam a ver "embrulho" em todo lugar. O módulo functools tem a solução: o decorador @wraps ("embrulha"), que você põe no embrulho e que copia para ele o nome, a docstring e outros dados da original.
from functools import wraps
def registrar(funcao):
@wraps(funcao)
def embrulho(*args, **kwargs):
print(f"Chamando {funcao.__name__}")
return funcao(*args, **kwargs)
return embrulho
@registrar
def somar(a, b):
"""Soma dois números."""
return a + b
print(somar.__name__)
print(somar.__doc__)
print(somar(2, 3))
$ python3 wraps_ok.py somar Soma dois números. Chamando somar 5
Repare que @wraps(funcao) tem parênteses: é um decorador que recebe um argumento (a original), como o @repetir(3) da seção 7. Daqui para a frente, todo decorador do guia usa @wraps. É o costume, e custa uma linha.
6. Um @cronometro
Um uso clássico: medir quanto tempo uma função leva. O time.perf_counter() ("contador de desempenho") devolve um relógio em segundos, bem preciso; a diferença entre duas leituras é o tempo que passou.
import time
from functools import wraps
def cronometro(funcao):
@wraps(funcao)
def embrulho(*args, **kwargs):
inicio = time.perf_counter()
resultado = funcao(*args, **kwargs)
fim = time.perf_counter()
print(f"{funcao.__name__} levou {fim - inicio:.4f} s")
return resultado
return embrulho
@cronometro
def somar_ate(n):
total = 0
for i in range(n):
total += i
return total
@cronometro
def somar_ate_rapido(n):
return sum(range(n))
print(somar_ate(1_000_000))
print(somar_ate_rapido(1_000_000))
$ python3 cronometro.py somar_ate levou 0.0359 s 499999500000 somar_ate_rapido levou 0.0098 s 499999500000 $ python3 cronometro.py somar_ate levou 0.0367 s « o tempo muda a cada execução 499999500000 somar_ate_rapido levou 0.0102 s 499999500000
Os dois jeitos de somar dão o mesmo resultado, mas o sum(range(n)) foi umas três vezes mais rápido: o laço do sum roda dentro do próprio Python, escrito em C, sem passar pelas linhas do seu programa a cada número. Os tempos mudam de uma execução para outra e de um computador para outro; o que vale é a comparação. O {… :.4f} mostra 4 casas decimais (aula f-strings).
Experimente: rode e compare os tempos do editor com os do terminal da Ana (o editor, que roda dentro do navegador, costuma ser algumas vezes mais lento). Depois troque o 1_000_000 por 5_000_000 e veja o tempo crescer.
import time
from functools import wraps
def cronometro(funcao):
@wraps(funcao)
def embrulho(*args, **kwargs):
inicio = time.perf_counter()
resultado = funcao(*args, **kwargs)
fim = time.perf_counter()
print(f"{funcao.__name__} levou {fim - inicio:.4f} s")
return resultado
return embrulho
@cronometro
def somar_ate(n):
total = 0
for i in range(n):
total += i
return total
@cronometro
def somar_ate_rapido(n):
return sum(range(n))
print(somar_ate(1_000_000))
print(somar_ate_rapido(1_000_000))
7. Decoradores com argumentos
E se o decorador precisar de uma configuração, como "repita 3 vezes"? Aí ele ganha mais um andar: uma função que recebe a configuração e fabrica o decorador (como o criar_saudacao da seção 1 fabricava funções).
from functools import wraps
def repetir(vezes):
def decorador(funcao):
@wraps(funcao)
def embrulho(*args, **kwargs):
for _ in range(vezes):
resultado = funcao(*args, **kwargs)
return resultado
return embrulho
return decorador
@repetir(3)
def buzinar():
print("Bi-bi!")
buzinar()
$ python3 repetir.py Bi-bi! Bi-bi! Bi-bi!
repetir(3)roda primeiro e devolvedecorador, que lembravezes = 3.- O
@aplica essedecoradoràbuzinar:buzinar = decorador(buzinar). buzinar()chama o embrulho, que chama a original 3 vezes. O_é o nome de costume para uma variável de laço que não vai ser usada.
Empilhando decoradores
Dá para pôr mais de um @ na mesma função. Eles se aplicam de baixo para cima (o mais perto do def embrulha primeiro), como camadas de papel:
def negrito(funcao):
def embrulho():
return "<b>" + funcao() + "</b>"
return embrulho
def italico(funcao):
def embrulho():
return "<i>" + funcao() + "</i>"
return embrulho
@negrito
@italico
def aviso():
return "Promoção"
print(aviso())
$ python3 empilhar.py <b><i>Promoção</i></b>
É o mesmo que aviso = negrito(italico(aviso)): o italico embrulha primeiro (fica por dentro), o negrito embrulha o resultado (fica por fora). Trocando a ordem dos @, sai <i><b>Promoção</b></i>.
Formas diferentes: decoradores prontos
Muitas vezes o decorador que você precisa já existe. Um dos mais úteis é o @cache, também do functools: ele guarda o resultado de cada chamada e, se a função for chamada de novo com os mesmos argumentos, devolve o resultado guardado sem calcular de novo. Na sequência de Fibonacci da aula Recursão, isso transforma uma conta que levaria anos numa conta instantânea:
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(80))
print(fibonacci.cache_info())
$ python3 cache.py 23416728348467685 CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)
O cache_info() conta: 81 cálculos de verdade (misses, "erros", um para cada n de 0 a 80) e 78 respostas tiradas do cache (hits, "acertos"). Sem o @cache, a mesma função faria dezenas de quatrilhões de chamadas repetidas.
| Forma | Quando usar |
|---|---|
funcao = decorador(funcao) | Para entender o que o @ faz, ou para decorar uma função que não é sua (de um módulo) sem mexer no def |
@decorador | Sempre que você escreve o def: fica visível logo em cima da função |
@decorador(argumento) | Quando o decorador precisa de configuração (@repetir(3)) |
| Decoradores prontos | @cache e @wraps (functools), @property, @classmethod, @staticmethod (aula @classmethod e @staticmethod), @dataclass (aula Dataclasses) |
Quando dá errado
return embrulho
def anunciar(funcao):
def embrulho():
print("[Atenção, clientes!]")
funcao()
@anunciar
def promocao():
print("Pão francês pela metade do preço!")
promocao()
$ python3 erro_sem_return.py
Traceback (most recent call last):
File "/home/ana/erro_sem_return.py", line 12, in <module>
promocao()
~~~~~~~~^^
TypeError: 'NoneType' object is not callable
"Um objeto NoneType não pode ser chamado". Sem return, o anunciar devolveu None, e o @ fez promocao = None. Na linha 12, promocao() tentou chamar o None como se fosse uma função. Como consertar: no fim do decorador, return embrulho.
return embrulho(), com parênteses
def anunciar(funcao):
def embrulho():
print("[Atenção, clientes!]")
funcao()
return embrulho()
@anunciar
def promocao():
print("Pão francês pela metade do preço!")
print("O programa começou.")
promocao()
$ python3 erro_chamou.py
[Atenção, clientes!]
Pão francês pela metade do preço!
O programa começou.
Traceback (most recent call last):
File "/home/ana/erro_chamou.py", line 14, in <module>
promocao()
~~~~~~~~^^
TypeError: 'NoneType' object is not callable
Dois sintomas juntos. O aviso apareceu antes de "O programa começou": com os parênteses, o decorador abriu o pacote na hora do @, rodando o embrulho; e devolveu o que o embrulho devolveu, None. Depois, a chamada de verdade, na linha 14, deu o mesmo erro do caso anterior. Como consertar: return embrulho, sem parênteses: devolva a função, não o resultado dela.
def anunciar(funcao):
def embrulho():
print("[Atenção, clientes!]")
funcao()
return embrulho
@anunciar
def promocao(produto):
print(f"{produto} pela metade do preço!")
promocao("Pão francês")
$ python3 erro_args.py
Traceback (most recent call last):
File "/home/ana/erro_args.py", line 13, in <module>
promocao("Pão francês")
~~~~~~~~^^^^^^^^^^^^^^^
TypeError: anunciar.<locals>.embrulho() takes 0 positional arguments but 1 was given
"embrulho(), local de anunciar, aceita 0 argumentos posicionais, mas recebeu 1". Repare no nome: quem foi chamado foi o embrulho, e ele foi escrito sem parâmetros. Como consertar: use o molde def embrulho(*args, **kwargs): e funcao(*args, **kwargs).
def registrar(funcao):
def embrulho(*args, **kwargs):
print(f"Chamando {funcao.__name__}")
funcao(*args, **kwargs)
return embrulho
@registrar
def somar(a, b):
return a + b
total = somar(2, 3)
print("total =", total)
$ python3 erro_none.py Chamando somar total = None
A soma foi feita, mas o 5 se perdeu no caminho: o embrulho chamou a original e jogou fora o que ela devolveu. Como o embrulho não tem return, devolveu None (aula return). Como consertar: return funcao(*args, **kwargs), ou guarde em resultado e devolva no fim.
@ de um decorador sem argumentos
$ python3 erro_parenteses.py
Traceback (most recent call last):
File "/home/ana/erro_parenteses.py", line 8, in <module>
@anunciar()
~~~~~~~~^^
TypeError: anunciar() missing 1 required positional argument: 'funcao'
Um anunciar como o da seção 2, usado com @anunciar() na linha 8. "Falta 1 argumento posicional obrigatório: funcao". Com parênteses, o Python chama anunciar() primeiro, sem nada, como faria com @repetir(3). Repare que o erro aponta a linha do @: ele acontece quando o Python passa pelo def, antes de qualquer chamada. Como consertar: @anunciar, sem parênteses. Os parênteses só vão quando o decorador foi feito para receber argumentos.
Referência rápida
| Escrita | O que faz |
|---|---|
f × f() | A função (o objeto) × chamar a função |
@deco em cima de def f | O mesmo que f = deco(f) logo depois do def |
@deco(arg) | O mesmo que f = deco(arg)(f) |
@a @b def f | f = a(b(f)): o de baixo embrulha primeiro |
def embrulho(*args, **kwargs): | Aceita quaisquer argumentos |
return funcao(*args, **kwargs) | Repassa os argumentos e devolve o resultado |
@wraps(funcao) | (de functools) copia nome e docstring da original para o embrulho |
@cache | (de functools) guarda resultados já calculados |
time.perf_counter() | Relógio preciso, em segundos, para medir intervalos |
| Sintoma | Causa provável |
|---|---|
TypeError: 'NoneType' object is not callable | O decorador não tem return embrulho (ou tem return embrulho()) |
O "antes" aparece na hora do @ | return embrulho() com parênteses |
embrulho() takes 0 positional arguments but N were given | O embrulho não tem *args, **kwargs |
A função decorada devolve None | O embrulho não devolve o resultado da original |
missing 1 required positional argument: 'funcao' | @deco() com parênteses num decorador sem argumentos |
__name__ é embrulho, docstring None | Faltou o @wraps(funcao) |
Exercícios
1. O que o @ faz
Escreva a linha equivalente a @anunciar em cima de def aviso():.
2. O molde
Complete o embrulho para aceitar quaisquer argumentos e devolver o resultado:
funcao(*args, **kwargs)
3. A etiqueta certa
Qual decorador do functools faz o embrulho manter o __name__ da original? Escreva só o nome.
4. Quando o corpo do decorador (a função anunciar) roda?
5. Com @negrito na primeira linha e @italico na segunda, em cima de def aviso(), qual é o equivalente?
6. Por que um @cronometro não serve para um teste com resultado exato, como "deve mostrar 0.0359 s"?
Missão final: quantas vendas?
Missão: o decorador contar_chamadas deve contar quantas vezes a função foi chamada. Ele guarda o número num atributo do próprio embrulho, embrulho.chamadas (funções são objetos, e objetos podem ganhar atributos). Faça três coisas: (1) dentro do embrulho, some 1 em embrulho.chamadas; (2) mostre f"{funcao.__name__}: chamada número {embrulho.chamadas}"; (3) ponha @wraps(funcao) em cima do def embrulho, para a última linha mostrar Nome: vender.
from functools import wraps
def contar_chamadas(funcao):
def embrulho(*args, **kwargs):
# 1. some 1 em embrulho.chamadas
# 2. mostre "nome: chamada número N"
return funcao(*args, **kwargs)
embrulho.chamadas = 0
return embrulho
@contar_chamadas
def vender(produto):
return f"Vendido: {produto}"
print(vender("pão"))
print(vender("leite"))
print(vender("café"))
print("Nome:", vender.__name__)
from functools import wraps
def contar_chamadas(funcao):
@wraps(funcao)
def embrulho(*args, **kwargs):
embrulho.chamadas += 1
print(f"{funcao.__name__}: chamada número {embrulho.chamadas}")
return funcao(*args, **kwargs)
embrulho.chamadas = 0
return embrulho
@contar_chamadas
def vender(produto):
return f"Vendido: {produto}"
print(vender("pão"))
print(vender("leite"))
print(vender("café"))
print("Nome:", vender.__name__)
A solução mostra, para cada venda, vender: chamada número 1, 2, 3 antes do Vendido: …, e termina com Nome: vender. Sem o @wraps, a última linha seria Nome: embrulho.
Resumo
- Funções são objetos:
fé a função,f()é a chamada. Dá para guardar, passar e devolver funções, e criar uma função dentro de outra. - Um decorador recebe uma função e devolve outra, o embrulho, que faz algo antes e depois de chamar a original.
@decoem cima dodefé o mesmo quef = deco(f). O decorador roda uma vez; o embrulho, a cada chamada.- O molde:
@wraps(funcao),def embrulho(*args, **kwargs):, o antes,resultado = funcao(*args, **kwargs), o depois,return resultado; e, no fim,return embrulhosem parênteses. - Decorador com configuração tem três andares (
@repetir(3)); empilhados, o de baixo embrulha primeiro. - Prontos para usar:
@cache,@wraps,@property,@classmethod,@staticmethod,@dataclass.