Primeiramente, por que falar sobre decoradores? Vamos lá, vou te convencer:
- Usamos
python
em produção na T10 - Adotamos o princípio DRY
- Decoradores podem ser ótimas ferramentas para se aplicar DRY
- Adotamos alguns frameworks que fazem uso extensivo de decoradores
- Porque te faz pensar em um nível alto de execução de programas
Decidimos redigir este artigo para apresentar decoradores sob dois pontos de vista, a fim de introduzir, além desse açucar sintático, o entendimento sobre funções de ordem maior em Python:
- Da pilha de chamadas
- Do tempo
Este artigo é baseado na excelente palestra de Rauven M. Lerner na PyCon de 2019, em Ohio.
Sugerimos que durante essa leitura estejam com um REPL do
python
aberto (recomendamos oipython
) para seguir interativamente, e fixar ideias que durem mais que os 10min de leitura
Breve apresentação
Em termos simples, um decorador é uma forma de adicionar informações à uma função. É por isso que recebeu este nome, porque decora uma função (na verdade, vamos ver que não apenas funções).
Começamos apresentando, para aqueles que não conhecem, o que é um decorador. Considere uma função simples de adição:
|
|
Desta forma, uma função decoradora é uma função que recebe e retorna uma função –um objeto do tipo função:
|
|
Vemos que, por meio da função decoradora deco
, conseguimos subir um nível na execução de chamadas e ganhar controle sobre o que acontece quando add
é chamada.
Concluímos, então, que deco
está uma ordem acima de add
.
Um pouco de história
Ainda não apresentamos o decorador, de fato. Não tem nada de novo no exemplo acima se já entendemos o conceito de funções de ordem maior. Python sempre conseguiu implementar essas chamadas porque as funções, em Python, são cidadãos de primeira classe.
No entanto, o que nem sempre existiu foi a sintaxe abaixo:
|
|
A motivação principal para sua implementação era ter uma forma mais limpa de se aplicar os métodos staticmethod()
e classmethod()
na declaração de métodos estáticos em classes. A sintaxe foi proposta na PEP 318 e foi aprovada e implementada na versão 2.4. A PEP é bem descritiva e mostra as diversas opiniões (desde o nome “decoradores” à escolha do caractere “@") sobre este incremento na linguagem.
““Chamáveis””
(Boa) parte da resistência em redigir um artigo técnico vêm da falta de criatividade em criar nomes não-toscos para termos que, originalmente, fazem muito mais sentido em inglês. Portanto, convenciona-se (no escopo deste artigo) que o termo chamáveis é a tradução literal de callables.
Bom, o que é um chamável em python? É qualquer objeto que implemente a função __call__()
.
Como validamos isso? Vamos fazer uso do método built-in hasattr
que nos conta se um determinado objeto possui um determinado atributo:
|
|
E uma classe? (Vamos fazer uso de outro método built-in, especificamente, para verificar chamáveis)
|
|
Nos interessa saber o que é um chamável porque conseguimos decorar –e controlar o comportamento– de qualquer chamável.
Decoradores simples
Quando uma função é definida,
|
|
Ao passar pelo keyword def
, acontecem (essencialmente) duas coisas:
- Cria o nome
add
no namespace atual- Cria-se um escopo para
add
- Cria-se um escopo para
- Associa-se ao nome
add
um objeto do tipo “função definida pelo usuário” (user-defined function)
Este objeto-função é este aqui:
|
|
Portanto, entendemos que o nome add
e o objeto de função associado à add
são coisas distintas: Um é o nome e o outro é o objeto. É “dentro” do objeto de função que está, por exemplo, o código da função:
|
|
Análise dos chamáveis de um decorador
Quando usamos um decorador, devemos entender que
|
|
é semânticamente idêntico à fazer isto:
|
|
Sugestão do autor: sempre que for flagrado um decorador, realizar esta tradução
E que, independente da forma como escrevemos, estamos lidando com três chamáveis:
- A função decoradora:
deco
- A função que está sendo decorada:
add
- A função (valor) retornada pela expressão:
deco(add)
Vale a pena ressaltar porque o último chamável é implícito. Ele é, na mesma linha, retornado e associado, novamente, ao nome add
.
Estes três chamáveis ficam implícitos quando analisamos a definição de deco
:
|
|
Análise de execução temporal
Para fins didáticos, faz sentido separar os decoradores em dois momentos, do ponto de vista de quando o interpretador passa por dois trechos:
Trecho da função decoradora
Quando o interpretador passa pelo trecho:
|
|
Ele segue para a definição de deco
:
|
|
Que associa, em seu namespace, o parâmetro fun
ao objeto função associado ao nome add
. Este trecho é executado apenas na declaração da função decorada.
Trecho da função decorada
Quando, depois de executar o bloco acima, o interpretador passa por qualquer trecho que chama add
:
|
|
Ele segue para a definição interna do decorador, ou seja, do embrulho da função:
|
|
Que ilustra com clareza o motivo de deco
estar a uma ordem acima de add
(ou qualquer outra função que deco
decore).
Para entender como a função interna,
wrapper
, tem acesso à função objeto armazenada no nomefunc
, é válido conhecer a ordem de resolução de escopos do Python: LEGB — Local Enclosed Global Built-in
E esse
*args
aí, hein? É assunto para outro post. Neste contexto, são formas de definir argumentos arbitrários para funções para que o decorador possa ser usado em qualquer função
Uma função que controla outras funções
O grande “takeaway” dos decoradores é este: a capacidade de escrever funções que controlam o comportamento de outras funções.
Imagine que seja atribuído a você fazer várias requisições à uma API externa para enriquecimento de uma base de dados. Consigo imaginar alguns exemplos em que os decoradores seriam úteis:
- Encapsular uma de-serialização padrão da resposta
- Metrificar o tempo que cada requisição leva
- Validar se o cabeçalho de requisição está com as entradas necessárias
- Definir um limite de consumo de um determinado endpoint
- ad infinitum…
Vamos implementar, a título de ilustração, como seria um decorador que controla o limite de consumo de um determinado endpoint:
|
|
Assim, na implementação do consumo de uma informação específica:
|
|
Permite que a função query_property
seja executada no máximo 5 vezes.
Decoradores com argumentos
Imagine que queremos definir um decorador dinâmico. Isto é, que receba argumentos de controle definidos pelo usuário.
Adotando o exemplo anterior, desejamos que o limitador funcione por um parâmetro max_calls
de vezes, em vez de um valor constante.
|
|
Que significa executar:
|
|
O que, podemos ver, aumenta mais um nível de ordem de execução. Em outras palavras, ao incluir um argumento no decorador, aumenta-se também mais um chamável.
Quando tentamos implementar a função do decorador fica mais evidente os quatro chamáveis:
|
|
Ou seja, os chamáveis são:
- A função decoradora:
limit_usage
- O resultado da expressão
limit_usage(15)
:outer_wrapper
- O resultado da expressão
limit_usage(15)
aplicada à funçãoquery_property
:inner_wrapper
- O resultado da função query_property
func
Do ponto de vista temporal, conseguimos separar em três momentos: um para cada ordem de execução:
-
Quando o interpretador aplica o argumento do decorador na função decoradora:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Só são executados os trechos marcados def limit_usage(max_calls): def outer_wrapper(func): counter = 0 def inner_wrapper(*args, **kwargs): nonlocal counter url = kwargs["url"] if counter >= max_calls: raise RuntimeException(f"Exceeded calls to {url}") return None counter += 1 result = func(*args, **kwargs) return result return inner_wrapper return outer_wrapper
Ao final desta execução, é como se voltássemos ao decorador simples, em que o valor de
max_calls
era uma constante. A diferença, agora, é que omax_calls
se torna uma constante dentro do escopo dequery_property
. -
Quando a função decoradora se associa à função decorada:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Só são executados os trechos marcados def limit_usage(max_calls): def outer_wrapper(func): counter = 0 def inner_wrapper(*args, **kwargs): nonlocal counter url = kwargs["url"] if counter >= max_calls: raise RuntimeException(f"Exceeded calls to {url}") return None counter += 1 result = func(*args, **kwargs) return result return inner_wrapper return outer_wrapper
Nesta execução já temos o mesmo comportamento observado no decorador simples. Sem novidades.
-
Quando o interpretador executa a função decorada:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# Só são executados os trechos marcados def limit_usage(max_calls): def outer_wrapper(func): counter = 0 def inner_wrapper(*args, **kwargs): nonlocal counter url = kwargs["url"] if counter >= max_calls: raise RuntimeException(f"Exceeded calls to {url}") return None counter += 1 result = func(*args, **kwargs) return result return inner_wrapper return outer_wrapper
Este é o momento que é executado toda as vezes que
func()
é chamada
Indo um pouco longe (que também é perto)
Os familiarizados com cálculo com certeza se depararam com a regra da cadeia para resolver derivadas. E, quando sobrava a resolução pela regra da cadeia, –em consenso com os colegas de turma– era baba, fácil de resolver. A ideia em trazer a analogia é para tentar simplificar a compreensão de funções compostas.
O que estamos fazendo aqui é basicamente a mesma coisa: associando funções compostas. E, ao adicionar argumentos à um decorador, seria equivalente à aumentar o grau de função.
Portanto podemos dizer que:
$$(f\circ g)' = (f'\circ g) \cdot g'$$
é o mesmo, em Python:
|
|
Fechando tudo
Decoradores e funções de ordem maior, em geral, é um ferramental (disponível em várias linguagens) que possibilita um grau maior de liberdade e de flexibilidade ao programador.
Presentes em vários frameworks, o entendimento de seu funcionamento permite que o programador seja capaz de fazer mais com menos. Com o que já existe. Alguns exemplos:
- Parametrização de testes no
pytest
:1 2 3 4 5
@pytest.mark.parametrize( "test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)] ) def test_eval(test_input, expected): assert eval(test_input) == expected
- Autenticação de endpoints no
django
:1 2 3
@permission_required(["GET", "POST"]) def my_view(request): ...
- Definição de endpoints no
aiohttp
:1 2 3
@routes.get('/') async def hello(request): return web.Response(text="Hello, world")
Espero que este tutorial tenha, de alguma forma, clareado dúvidas e favorecido o entendimento sobre o assunto 😄
Com carinho,
Rui