Decoradores
Decoradores são um elemento significante do Python, também são conhecidos como meta-programação, para simplificarmos sua ideia, podemos dizer que eles são funções que modificam a funcionalidade de uma outra função, eles nos ajudam a deixar o código menor e mais Pythônico (legível ao modo Python). O conceito de decoradores pode ser inicialmente um pouco difícil de capturarmos, mas vamos por partes.
Antes de compreendermos os decoradores, é interessante que entendamos como as funções funcionam. Lembre que uma função retorna um valor baseado nos argumentos fornecidos a ela. Vejamos um simples exemplo:
def saudar(nome='Gabriel'):return f'Saudações, {nome}!'
Podemos agora chamar essa função sem um argumento (pois já existe um argumento padrão nela), ou com um argumento:
saudar() # 'Saudações, Gabriel!'saudar('Rafael') # 'Saudações, Rafael!'
Em geral, as funções em Python também podem ter efeitos colaterais, em vez de apenas transformar um input em um output. A função print() é um exemplo básico disso: ela retorna None
enquanto tem o efeito colateral de enviar algo para o console. Porém, para entender os decoradores, basta pensar nas funções como algo que transforma determinados argumentos em um valor.
Também podemos atribuir uma função a uma variável, para isso, não usamos parenteses, dessa maneira estamos guardando apenas a referência ao objeto função.
saudacao = saudarprint(saudacao) # <function saudar at 0x7f7ab6fca290>saudacao('Miguel') # 'Saudações, Miguel!'
Podemos por exemplo deletar a referência em memória da antiga função:
del saudarsaudar() # NameError: name 'saudar' is not defined
Veja que se tentarmos invocá-la írá ocorrer um NameError
, mas veja que ainda podemos chamar a outra referência que temos:
saudacao() # 'Saudações, Gabriel!'
Veja que mesmo deletando a função antiga, ainda conseguimos executar a função saudacao()!
Funções dentro de Funções
Em Python, funções são first-class objects. Isso significa que as funções podem ser transmitidas e usadas como argumentos, assim como qualquer outro objeto (string, int, float, listas e assim por diante).
Vamos começar definindo uma simples função:
def func(string):def wrapper():print("Iniciada")print(string)print("Finalizada")return wrapper()
Observe que temos uma função de nome func() que recebe uma string como argumento e dentro dela temos uma função de nome wrapper() que não recebe nenhum argumento e imprime "Iniciada"
, a string passada para a função func() e "Finalizada"
e finalmente retorna a função wrapper() (ela mesma) invocando-a. Podemos usá-la da seguinte forma:
f = func("Hello World")# Iniciada# Hello World# Finalizada
Vamos agora modificá-la para que a função wrapper() não retorne a ela mesmo invocando-a, mas dessa vez apenas o objeto função.
def func(string):def wrapper():print("Iniciada")print(string)print("Finalizada")return wrapper
Dessa vez para usá-la temos de usar o seguinte procedimento:
f = func("Hello World")print(f) # <function func.<locals>.wrapper at 0x7f7ab6fca0e0>f()# Iniciada# Hello World# Finalizada
Também existe a opção de chamá-la da seguinte forma:
func("Hello World")()# Iniciada# Hello World# Finalizada
Até então tudo bem, nada que venha a nos surpreender. Mas e se desejarmos passar uma outra função como argumento para func()? Vamos definir duas novas funções para testarmos:
def func(f):def wrapper():print("Iniciada")f()print("Finalizada")return wrapperdef f1():print("Função f1() chamada!")def f2():print("Função f2() chamada")
Observe que modificamos a função wrapper() e agora ela está invocando a função f() (esta que passaremos como argumento). Vamos agora chamar a função func() passando as funções f1() e f2() como argumento:
f1 = func(f1)f2 = func(f2)print(f1) # <function func.<locals>.wrapper at 0x7f7ab702ab90>print(f2) # <function func.<locals>.wrapper at 0x7f7ab702a950>f1()# Iniciada# Função f1() chamada!# Finalizadaf2()# Iniciada# Função f2() chamada# Finalizada
Basicamente este é o conceito de decoradores, estamos usando a função func() para alterar o comportamento das funções f1() e f2(), porém existe uma maneira mais interessante de definí-los, em outras palavras, mais Pythônica.
Decorando Funções
Vamos decorar as funções f1() e f2() usando a sintaxe @
:
@funcdef f1():print("Função f1() chamada!")@funcdef f2():print("Função f2() chamada")
Dessa vez podemos chamá-las diretamente e teremos o mesmo efeito anterior:
f1()# Iniciada# Função f1() chamada!# Finalizadaf2()# Iniciada# Função f2() chamada# Finalizada
Faremos agora outra modificação, dessa vez as funções wrapper() e f1() passam a receber um argumento e a função passada como argumento para func() passa a ser invocada com esse argumento:
def func(f):def wrapper(x):print("Iniciada")f(x)print("Finalizada")return wrapper@funcdef f1(x):print(f"O valor de x é = {x}")@funcdef f2():print("Função f2() chamada")
Vejamos agora o que ocorre se invocarmos as funções f1() e f2():
f1(7)# Iniciada# O valor de x é = 7# Finalizadaf2() # TypeError: wrapper() missing 1 required positional argument: 'x'
f1() é chamada e nos imprime o valor como esperado, porém f2() "quebra", não somo capazes de chamá-la, pois a função wrapper() espera um argumento. Para solucionar este problema podemos usar o conceito de *args
e **kwargs
, vamos então masi uma vez modificar nossas funções:
def func(f):def wrapper(*args, **kwargs):print("Iniciada")f(*args, **kwargs)print("Finalizada")return wrapper@funcdef f1(x):print(f"O valor de x é = {x}")@funcdef f2():print("Função f2() chamada")f1(7)# Iniciada# O valor de x é = 7# Finalizadaf2()# Iniciada# Função f2() chamada# Finalizada
Para finalizarmos, vamos modificar nossas funções wrapper() e f1() para retornar um valor:
def func(f):def wrapper(*args, **kwargs):print("Iniciada")valor_retorno = f(*args, **kwargs)print("Finalizada")return valor_retornoreturn wrapper@funcdef f1(x, y):print(f"O valor de x é = {x}")print(f"O valor de y é = {y}")return y + xf1 = f1(5,33)# Iniciada# O valor de x é = 5# O valor de y é = 33# Finalizadaprint(f1) # 38
Outros Exemplos
Funções e métodos são chamados callable se for possível chamá-los. De fato, qualquer objeto que implemente o método especial __call__()
é um callable, então um decorador seria um callable que retorna um callable.
Então como já tinhamos dito na nossa introdução, um decorador recebe um função, adiciona alguma funcionalidade a ela e a retorna:
def embelezar(func):def interno():print("Fui decorado")func()return internodef normal():print("Eu sou normal")normal() # Eu sou normalbonito = embelezar(normal)bonito()# Fui decorado# Eu sou normal
Veja que a função normal() foi decorada e demos o nome à função retornada, que se chamou bonito. Uma forma mais eficaz de usarmos os decoradores é usando o símbolo @
junto do nome da função decoradora e colocar ele em cima da definição da função a ser decorada. Por exemplo:
@embelezardef normal():print("Eu sou normal também")normal()# Fui decorado# Eu sou normal também
Decorando Funções com Parâmetros
O último decorador que escrevemos foi bastante simples, só funcionava com funções sem parâmatros, apenas para ilustrarmos o conceito, mas e se tivermos funções que operam com parâmetros?
def divisao(x, y):return x / y
A função recebe dois parâmetros x e y, sabemos que se passarmos 0 para y ocorrerá um erro.
print(divisao(10,2)) # 5.0print(divisao(3,0)) # ZeroDivisionError: division by zero
Agora vamos fazer um decorador para resolvermos esse problema.
def divisao_inteligente(func):def interior(x,y):print("Será feita uma divisão de {0} por {1}".format(x,y))if y == 0:print("Impossível dividir")returnreturn func(x,y)return interior@divisao_inteligentedef divisao(x,y):return x / ydivisao(3,3)# Será feita uma divisão de 3 por 3# 1.0divisao(3,0)# Será feita uma divisão de 3 por 0# Impossível dividir
Medindo Desempenho
Podemos criar um decorar que é capaz de medir quanto tempo uma função leva para rodar:
import timedef timer(func):def wrapper(*args, **kwargs):inicio = time.time()valor_retorno = func()total = time.time() - inicioprint(f"Tempo: {total}")return valor_retornoreturn wrapper
Vamos definir duas funções para medí-las:
@timerdef t1():for _ in range(10_000_000):pass@timerdef t2():time.sleep(2.3)
Finalmente, vamos obter o tempo de cada uma:
t1() # Tempo: 0.36320018768310547t2() # Tempo: 2.302194118499756
Com esse estudo podemos considerar que decoradores são de certa forma um pouco complexos, mas que nos trazem novas possibilidades de trabalharmos em cima das funções, modificando seu comportamento. Eles são muito comuns também em frameworks web, como Flask e Bottle por exemplo.