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.

Você vai aprender
  • 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 *args e **kwargs e a devolver o resultado.
  • Por que usar functools.wraps.
  • Um @cronometro com time.perf_counter(), decoradores com argumentos, empilhados, e o @cache pronto.
  • Os erros reais: 'NoneType' object is not callable, takes 0 positional arguments, missing 1 required positional argument e o resultado que vira None.

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 presenteNo Python
O livroA função original (promocao)
Quem embrulhaO decorador (anunciar)
O pacote prontoA função nova que o decorador devolve (embrulho)
O cartão e o laçoO 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ção saudar, 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 o key= do sorted e com o map (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 promocao original para o anunciar e cola a etiqueta promocao no 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) e kwargs é {}; em preco_final(10.0, desconto=1.5), args é (10.0,) e kwargs é {'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, total seria None (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!
  1. repetir(3) roda primeiro e devolve decorador, que lembra vezes = 3.
  2. O @ aplica esse decorador à buzinar: buzinar = decorador(buzinar).
  3. 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.

FormaQuando 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
@decoradorSempre 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

Esqueceu o 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.

O embrulho não aceita os argumentos
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).

O resultado sumiu (sem erro nenhum)
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.

Parênteses no @ 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

EscritaO que faz
f × f()A função (o objeto) × chamar a função
@deco em cima de def fO mesmo que f = deco(f) logo depois do def
@deco(arg)O mesmo que f = deco(arg)(f)
@a @b def ff = 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
SintomaCausa provável
TypeError: 'NoneType' object is not callableO 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 givenO embrulho não tem *args, **kwargs
A função decorada devolve NoneO 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 NoneFaltou 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:

def embrulho():
     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.
  • @deco em cima do def é o mesmo que f = 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 embrulho sem 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.