Python – Decorators

Navegação

Objetivo

Este post tem o objetivo de mostrar de forma prática e não aprofundada o uso de decorators em Python.

Funções de primeira classe

Ser uma função de primeira classe permite que as funções sejam passadas na chamadas de outras funções, sejam armazenadas em variáveis e retornadas na execução de outras funções.

Instâncias de funções

Em Python, depois que uma função é definida, o nome da função passa a se referir a instância da função. Esta “instância” seria o endereço de memória onde estariam armazenados parâmetros e variáveis da função.

O código abaixo define uma função e printa o endereço de memória que está sendo referenciado pelo nome da função.

Repare que no código a função não é executado e somente sua definição faz com que ela seja armazenada em memória.

				
					def funcao1():
    print("Execução da funcao1")

print(funcao1)
				
			
				
					<function funcao1 at 0x000001E40D3EA320>
				
			

Como as funções em Python são de primeira classe, eu posso atribuir esta função a uma variável e mostrar na tela o conteúdo da variável, que obviamente será o mesmo endereço de memória que armazena a definição da função.

				
					def funcao1():
    print("Execução da funcao1")

print(funcao1)

varFuncao1 = funcao1
print(varFuncao1)
				
			
				
					<function funcao1 at 0x00000154EC7EA320>
<function funcao1 at 0x00000154EC7EA320>
				
			

Também é possível retornar uma função a partir da execução de outra função.

O código abaixo executa a função get_funcao1 que retorna a função funcao1, isto é, retorna o endereço de memória referenciado pelo nome funcao1.

				
					def funcao1():
    print("Execução da funcao1")

print(funcao1)

def get_funcao1():
    return funcao1

varFuncao1 = funcao1
print(varFuncao1)
				
			
				
					<function funcao1 at 0x000001BB132BA320>
<function funcao1 at 0x000001BB132BA320>
				
			

Uma função também pode ser passada como argumento para outra função. No código abaixo a função show_funcao1 recebe funcao1 como parâmetro e printa o endereço de memória referenciado pelo nome funcao1.

				
					def funcao1():
    print("Execução da funcao1")

def show_funcao1(func):
    print(func)

varFuncao1 = show_funcao1(funcao1)
				
			
				
					<function funcao1 at 0x000001EA171FA320>
				
			
Invocando funções

Pode parecer óbvio, mas funções podem ser invocadas e o termo inglês que define este comportamento é “callable”. Isto é, dizer um objeto é callable quer dizer que ele pode ser invocado.

A forma de invocar funções é adicionando parênteses após o nome da função, ou melhor, após o endereço de memória referenciado pela função.

O código abaixo invoca a função de duas formas. A primeira chamada é feita de forma direta com o nome da função seguido de parênteses. A segunda forma utiliza uma variável que recebeu a função.

				
					def funcao1():
    print("Execução da funcao1")

funcao1()  # primeira forma

varFuncao1 = funcao1
varFuncao1()  # segunda forma
				
			
				
					Execução da funcao1
Execução da funcao1

				
			
Funções são objetos

Funções são instâncias (objetos) da classe embutida function.

O código abaixo define uma função func2 e mostra que ela é um objeto da classe function.

				
					def funcao2():
    print("Execução da funcao2")

print(type(funcao2))
				
			
				
					<class 'function'>
				
			

Como sabemos, objetos em Pyhton podem conter atributos e sendo uma função um objeto ela também pode ter atributos.

O código abaixo é semelhante ao código anterior, mas adiciona a função embutida dir() que mostra os atributos de um objeto.

				
					def funcao2():
    print("Execução da funcao2")

print(type(funcao2))

print(dir(funcao2))
				
			
				
					<class 'function'>
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

				
			

Sendo uma função um objeto, é possível setar um atributo chamado “algumacoisa”, por exemplo, tendo o valor “Hello”.

O código abaixo seta um atributo para a função funcao2 e chama a função embutida dir() para mostrar que o nome do atributo agora está listado no conjunto de atributos do objeto.

Repare que “algumacoisa” aparece na resposta com sendo o último atributo.

				
					def funcao2():
    print("Execução da funcao2")

funcao2.algumacoisa = 'Hello'

print(type(funcao2))

print(dir(funcao2))
				
			
				
					<class 'function'>
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'algumacoisa']

				
			

O código a seguir seta o valor de “algumacoisa” fora do corpo da função e depois printa o valor de “algumacoisa” dentro do corpo da função.

Isto mostra que o nome da função e o atributo “algumacoisa” tem um escopo global, isto é, existem mesmo antes da execução da função.

				
					def funcao2():
    print("Execução da funcao2")
    print(funcao2.algumacoisa)

funcao2.algumacoisa = 'Hello'

funcao2()
				
			
				
					Hello
				
			

Também é possível acessar os atributos da função dentro da própria função.

O código abaixo alterada o código anterior setando um novo valor para “algumacoisa”.

				
					def funcao2():
    funcao2.algumacoisa = 'Bye'
    print("Execução da funcao2")
    print(funcao2.algumacoisa)

funcao2.algumacoisa = 'Hello'

funcao2()
				
			
				
					Execução da funcao2
Bye
				
			
Escopo de variáveis

Como veremos na próxima seção, funções podem definir outras funções dentro do seu escopo.

O exemplo abaixo mostra de forma simples como as variáveis podem ser usadas por funções dentro de funções.

				
					def outer_func(name):
    print(f'Hi, {name}. Printing from Outer func.')
    def inner_func():
        print(f'Hi, {name}. Printing from Inner func.')
    inner_func()

outer_func('BRUNO')
				
			
				
					Hi, BRUNO. Printing from Outer func.
Hi, BRUNO. Printing from Inner func.
				
			

A função inner_func pode acessar a variável name, que foi recebida como parâmetro pela função outer_func.

Por outro lado, se for definida uma outra variável name dentro de inner_func, ela terá um escopo local dentro da função e não será igual a variável name de outer_func.

				
					def outer_func(name):
    print(f'Hi, {name}. Printing from Outer func.')
    def inner_func():
        name = 'ARTHUR'
        print(f'Hi, {name}. Printing from Inner func.')
    inner_func()
    print(f'Hi, {name}. Printing from Outer func.')

outer_func('BRUNO')
				
			
				
					Hi, BRUNO. Printing from Outer func.
Hi, ARTHUR. Printing from Inner func.
Hi, BRUNO. Printing from Outer func.
				
			

Se desejar alterar o valor da variável name que foi criada no escopo da função outer_func, é preciso declarar name como não local dentro de inner_func.

				
					def outer_func(name):
    print(f'Hi, {name}. Printing from Outer func.')
    def inner_func():
        nonlocal name
        name = 'ARTHUR'
        print(f'Hi, {name}. Printing from Inner func.')
    inner_func()
    print(f'Hi, {name}. Printing from Outer func.')

outer_func('BRUNO')
				
			
				
					Hi, BRUNO. Printing from Outer func.
Hi, ARTHUR. Printing from Inner func.
Hi, ARTHUR. Printing from Outer func.
				
			

O exemplo abaixo demonstra o escopo de variáveis com mais detalhes.

				
					x = 5  # variável "global"
y = 3
print("Antes da chamada de wrapper", x, y)
def wrapper():
    def inner():
        # declaração global
        global x
        x = 6  # altera o valor de x global
        y = -2  # variável local dentro de inner e não altera y global
        # z é uma variável livre dentro de inner, isto é uma variável que foi definida fora de inner
        print("Inner", x, y, z)
    y = 1  # variável local dentro de wrapper e pode ser sobrescrita dentro de inner
    z = 0  # variável dentro de wrapper que pode ser usada em inner (escopo mais fechado)
    inner()
    print("Wrap", x, y, z)

wrapper()
				
			
				
					Antes da chamada de wrapper 5 3
Inner 6 -2 0
Wrap 6 1 0

				
			
Funções aninhadas

O Python permite que funções sejam executadas dentro de funções.

O código abaixo define a função display dentro da função say. A função display é chamada função aninhada (nested).

Dentro da função display você acessa o valor da variável saudacao a partir do seu escopo não local, isto é, foi acessada uma variável que não foi definida dentro do corpo da função display.

O Python chama a variável saudacao de variável livre (free variable).

Uma variável livre é uma variável que não é local e nem foi passada como um argumento da função.

Uma variável definida dentro do escopo da função pai será uma variável livre dentro de qualquer função aninhada.

				
					def say():
    saudacao = 'Hello'

    def display():
        print(saudacao)

    display()

say()
				
			
				
					Hello
				
			
Closures

Quando você olha para a função display, você vê duas coisas:

  1. A própria função display
  2. A variável livre saudacao com o valor “Hello”

A combinação da função display com a variavel saudacao é chamada closure.

Por definição, um closure é uma função aninhada que referencia uma ou mais variáveis do escopo que a engloba.

No exemplo abaixo a função say retorna a função display ao invés de executá-la.

				
					def say():
    saudacao = 'Hello'

    def display():
        print(saudacao)

    return display

print()
				
			

Quando a função say retorna a função display, ela na verdade retorna o closure, isto é, retorna a função display e a variável saudacao.

Nem toda função aninhada é um closure. Para uma função aninhada ser considerada um closure as seguintes condições precisam estar presentes:

  • A função aninhada executa depois que a função pai finalizou.
  • A função aninhada tem acesso a variáveis não-locais que são do escopo da função pai.

O código abaixo modifica o código anterior retornando a chamada de say para a variável func, isto é, fazendo com que func seja o endereço de memória referenciado pela função display e assim, sendo func uma função, ela pode ser executada adicionando-se parênteses após o nome da variável.

Isto faz com que as duas condições estejam presentes e display seja um closure.

				
					def say():
    saudacao = 'Hello'

    def display():
        print(saudacao)

    return display

func = say()
func()
				
			
				
					Hello
				
			

Mas me parece que algo está estranho!

A função say executa e retorna o endereço de memória da função display. Até aí tudo certo. Mas quando func executa, a função say já completou a sua execução, então, como a função display sabe o valor de saudacao?

O escopo da função say deveria ter desparecido depois da sua finalização e a variável saudacao não deveria mais existir quando display é chamada através da variável func.

Em outras palavras, estando a variável saudacao dentro do escopo da função say, ela deveria ter sido destruída junto com o escopo da função, mas foi verificado que a execução de func() printa o valor da variável saudacao.

Mesmo que a função say seja excluída, o valor de saudação continua existindo.

				
					def say():
    saudacao = 'Hello'

    def display():
        print(saudacao)

    return display

func = say()
func()

del say  # deleta a função say.
func()  # o valor de saudacao continua existindo.
				
			
				
					Hello
Hello
				
			

Como isto é possível?

Python Cells e variáveis multi-escopo

O valor da variável saudacao é compartilhada entre dois escopos:

  1. A função say
  2. O closure

O rótulo saudacao está em dois diferentes escopos, entretanto, eles fazem referência ao mesmo objeto do tipo string com o valor “Hello”.

Para fazer isto, o interpretador do Python cria um objeto intermediário chamado cell.

Para encontrar o endereço de memória do objeto cell é possível usar __closure__.

				
					print(func.__closure__)
				
			
				
					(<cell at 0x00000195DB607D90: str object at 0x00000195DB5EDAF0>,)
				
			

O __closure__ retorna uma tupla. O primeiro endereço de memória é a Cell, enquanto o segundo endereço é do objeto string (hello) referenciado pela Cell.

Se buscar pelo endereço de memória do objeto string dentro da função say e do closure, irá ver que a referência é a mesma, isto é, o objeto string ‘Hello’.

				
					def say():
    saudacao = 'Hello'
    print(hex(id(saudacao)))

    def display():
        print(hex(id(saudacao)))
        print(saudacao)

    return display

func = say()
func()
				
			
				
					0x195db5edaf0
0x195db5edaf0
Hello
				
			

Quando o valor de saudacao é acessado, o Python irá fazer uma salto duplo para obter o valor da string, isto é, irá primeiro consultar o valor de Cell para depois acessar o valor da string ‘Hello’.

Na prática o que acontece é que quando say é compilado, o interpretador entende que display precisará utilizar a variável saudacao (local) mais tarde, então, “memoriza” a variável para que possa ser usada depois.

Assim, pode-se pensar em um closure como sendo uma função e, adicionalmente, um escopo estendido que contém variáveis livres.

Para visualizar as variáveis livres que um closure contém usamos o __code__.co_freevars.

				
					print(func.__code__.co_freevars)
				
			
				
					('saudacao',)
				
			

Fazendo um paralelo com POO, objetos são descritos como dados com métodos associados, enquanto clousers são funções com dados associados.

A seguir outro exemplo de closure.

				
					def divisor(y):
	def divisao(x):
		return x / y
	return divisao

d1 = divisor(2)
d2 = divisor(5)
d3 = divisor(4)

print(d1(20))
print(d2(20))
print(d3(20))
				
			
				
					10.0
4.0
5.0
				
			

Aqui d1, d2 e d3 tem diferentes instância do closure. Depois de cada execução de divisor, o valor de y fica “memorizado” e será usado quando a função divisao for posteriormente invocada.

Decorators

Em Python, um Decorator é um método que altera o comportamento de uma função ou método.

Um decorator utiliza o conceito de closure na sua implementação.

A seguir está o exemplo de um decorator simples.

				
					def verbose(func):
    def wrapper():
        print('Before', func.__name__)
        result = func()
        print('After', func.__name__)
        return result
    return wrapper

def hello():
    print('Hello')

hello = verbose(hello)

hello()
				
			
				
					Before hello
Hello
After hello
				
			

É importante entender o que a função verbose faz. Ela aceita uma função (func) como parâmetro e retorna uma nova função (wrapper).

Quando wrapper é invocada, ela imprime o nome da função(hello), executa a função hello, printa o nome novamente e retorna o resultado da função original(hello).

Perceba que o decorator utiliza um closure que nada mais é que a função wrapper + a variável livre func, que foi passada para a função pai (verbose).

Quando hello = verbose(hello) executa, a função verbose define a função wrapper e o parâmetro func(que é o endereço de memória da função hello()). Aqui a função wrapper não é executada, mas é retornada para a variável hello.

Quando wrapper é chamada por hello(), o closure wrapper entra em ação, isto é, o closure memorizou o valor de func da etapa anterior e agora pode utilizá-lo para chamar a função hello(), além de imprimir __name__ antes e depois de sua execução. Aqui, a função hello() está sendo decorada por verbose e tendo seu comportamento modificado.

Todos os outros decorators seguirão este mesmo padrão.

A seguir está um passo-a-passo detalhado da execução do código.

→ Início da execução.

→ A função verbose é definida e alocada em um endereço de memória.

→ A função hello é definida e alocada em um endereço de memória.

→ A função verbose é invocada, passando a função hello como argumento.

→ Dentro do código da função verbose, a função wrapper é definida e alocada em um endereço de memória.

→ A função wrapper (end. de memória) é retornado e atribuído à hello (que antes tinha o endereço de memória da função hello).

Na verdade o que é retornado é o closure que contém a função wrapper e a free variable func, que o Pyhton entende que será utilizada futuramente. Assim, quando wrapper for chamada, o valor de func  estará memorizado e não será perdido ao final da execução de verbose.

→ A função Hello é executada adicionando-se parênteses no final do nome.

→ Como Hello agora aponta para wrapper, wrapper é executada.

→ Dentro do código de wrapper, é impresso na tela o nome da função (func).

Como func havia sido “memorizada” pelo closure, o Python sabe que o valor de func é o endereço de memória da função hello.

→ O nome da função (hello) é impresso novamente.

→ É retornado o valor de result.

→ Fim da execução.

Para verificar que o closure foi criado na chamada na função verbose execute o seguinte código.

				
					def verbose(func):
    def wrapper():
        print('Before', func.__name__)
        result = func()
        print('After', func.__name__)
        return result
    return wrapper

def hello():
    print('Hello')

print(hello.__code__.co_freevars)  # verifica se há um closure criado antes da execução de verbose.
hello = verbose(hello)
print(hello.__code__.co_freevars)  # verifica que o closure só foi criado depois da execução de verbose.

hello()
				
			
				
					()
('func',)
Before hello
Hello
After hello
				
			

Repare que a linha que printa o closure só retorna o valor “func” depois da execução da função verbose.

Formas de aplicar um decorator

Um decorator só é útil quando aplicado a uma função. Há duas maneiras de se fazer isto.

A primeira é simplesmente invocar o decorator em uma função.

Assim, o decorator seria a função verbose e a função “decorada” seria hello.

				
					def hello():
    print('Hello')

hello = verbose(hello)
				
			

A segunda forma de decorar uma função é inserindo @verbose imediatamente antes da definição da função decorada.

				
					def verbose(func):
    def wrapper():
        print('Before', func.__name__)
        result = func()
        print('After', func.__name__)
        return result
    return wrapper

@verbose
def grite():
    print('HELLO!!!')

grite()
				
			
				
					Before grite
HELLO!!!
After grite
				
			

Com o código acima é possível verificar que qualquer função que seja decorada com @verbose terá seu comportamento modificado, isto é, printando o nome da função antes e depois de sua execução.

Um outro exemplo

Neste exemplo há 3 funções simples que “dormem” por 1, 3 e 5 segundos respectivamente. As funções imprimem na tela uma mensagem dizendo que irão dormir, depois “dormem” por um determinado tempo.

				
					import time

def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

durma_1seg()
durma_3seg()
durma_5seg()

				
			
				
					Dormirei por 1 segundo.
Dormirei por 3 segundos.
Dormirei por 5 segundos.

Process finished with exit code 0
				
			
Modificando a função e adicionando um elemento

No próximo exemplo foi adicionado um contador que mede o tempo, em segundos, da execução de cada função. Aqui estamos modificando o comportamento da função e adicionando mais um elemento, a medição do tempo.

				
					import time

def durma_1seg():
    tempo_inicio = time.time()
    print('Dormirei por 1 segundo.')
    time.sleep(1)
    tempo_fim = time.time()
    print("Duração da função: {}".format(tempo_fim-tempo_inicio))

def durma_3seg():
    tempo_inicio = time.time()
    print('Dormirei por 3 segundos.')
    time.sleep(3)
    tempo_fim = time.time()
    print("Duração da função: {}".format(tempo_fim - tempo_inicio))

def durma_5seg():
    tempo_inicio = time.time()
    print('Dormirei por 5 segundos.')
    time.sleep(5)
    tempo_fim = time.time()
    print("Duração da função: {}".format(tempo_fim - tempo_inicio))

durma_1seg()
durma_3seg()
durma_5seg()

				
			
				
					Dormirei por 1 segundo.
Duração da função: 1.0044081211090088
Dormirei por 3 segundos.
Duração da função: 3.0101377964019775
Dormirei por 5 segundos.
Duração da função: 5.004319190979004

Process finished with exit code 0
				
			

Perceba o código que recupera o tempo inicial, tempo final e mostra duração da execução das funções.

				
					def durma_5seg():
    tempo_inicio = time.time()
    print('Dormirei por 5 segundos.')    # <------ CÓDIGO ADICIONADO À FUNÇÃO
    time.sleep(5)
    tempo_fim = time.time()    # <------ CÓDIGO ADICIONADO À FUNÇÃO
    print("Duração da função: {}".format(tempo_fim - tempo_inicio))    # <------ CÓDIGO ADICIONADO À FUNÇÃO
				
			

Este código precisa ser repetido em todas as funções.

Adicionando outra função que modifica o comportamento de uma função existente

Para evitar repetição, poderia ser criada uma função específica para medir o tempo de execução de todas as outras funções.

Foi criada a função calcula_tempo() com o seguinte trecho de código:

				
					def calcula_tempo:
    tempo_inicio = time.time()
    # Código que executa a função de dormir.
    tempo_fim = time.time()
    print("Duração da função: {}".format(tempo_fim - tempo_inicio))

				
			

Perceba que aqui foi construída uma função (calcula_tempo) que pretende modificar o comportamento da função original durma_xseg, isto é, estamos colocando mais uma funcionalidade na função que já existia.

A questão aqui é como incluir a execução das 3 funções de “dormir” (dorme_xseg) entre as linhas tempo_inicio e tempo_fim, sem repetição de código e de forma que o tempo seja medido individualmente para cada uma das funções.

Uma opção seria usar o seguinte código:

				
					import time

def calcula_tempo(funcao_que_dorme):
    tempo_inicio = time.time()
    funcao_que_dorme()
    tempo_fim = time.time()
    print("Duração da função: {}".format(tempo_fim - tempo_inicio))

def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

calcula_tempo(durma_1seg)
calcula_tempo(durma_3seg)
calcula_tempo(durma_5seg)

				
			

Aqui, a função calcula_tempo recebe como parâmetro uma função que “dorme” e logo após recuperar o tempo_inicio faz a execução da função que foi recebida, mede o tempo_fim e mostra na tela a duração.

Na sequência do código são definidas as funções que “dormem” e no final a função calcula_tempo é chamada 3 vezes passando como parâmetro cada uma das funções que irão dormir.

Não há repetição de código e as funções que “dormem” tiveram seus comportamentos modificados inserindo a medição do tempo.

Desta forma, o fluxo na execução, por exemplo, da linha calcula_tempo(durma_1seg) seria assim:

				
					calcula_tempo(durma_1seg)
				
			

→ Executa calcula_tempo(durma_1seg).

→ A função calcula_tempo é invocada passando como parâmetro a função durma_1seg.

→ Já no código na função calcula_tempo, é registrado o tempo_inicio.

→ A função que foi passada como parâmetro (durma_1seg) é invocada.

→ Dentro do código da função durma_1seg, mostra na tela a mensagem que irá dormir.

→ Dorme por 1 segundo.

→ Retorna o controle para a função chamadora (calcula_tempo).

→ Registra o tempo_fim.

→ Mostra na tela a duração da execução.

→ Termina o programa.

O resultado da execução do código não foi alterado depois da criação da função calcula_tempo():

				
					Dormirei por 1 segundo.
Duração da função: 1.0072298049926758
Dormirei por 3 segundos.
Duração da função: 3.0055301189422607
Dormirei por 5 segundos.
Duração da função: 5.001744031906128

Process finished with exit code 0
				
			

Lindão, o código funciona perfeitamente. Mas perceba que a minha intenção inicial era modificar o comportamento da função durma_xseg adicionando a contagem de tempo, mas aqui meu código principal faz chamadas a função calcula_tempo e não a função durma_xseg.

Função que retorna uma função, decorators outra vez

A pergunta aqui é: e seu eu quiser modificar o comportamento da função durma_xseg adicionando um contador de tempo de execução, mas no meu código principal eu quiser continuar chamando a função durma_xseg ao invés da função calcula_tempo?

Como resposta a esta pergunta podemos novamente utilizar um decorator.

				
					import time

def calcula_tempo(funcao_que_dorme):
    def wrapper():
        tempo_inicio = time.time()
        funcao_que_dorme()
        tempo_fim = time.time()
        print("Duração da função: {}".format(tempo_fim - tempo_inicio))
    return wrapper

def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

durma_1seg = calcula_tempo(durma_1seg)
durma_3seg = calcula_tempo(durma_3seg)
durma_5seg = calcula_tempo(durma_5seg)

durma_1seg()
durma_3seg()
durma_5seg()

				
			
				
					Dormirei por 1 segundo.
Duração da função: 1.007664680480957
Dormirei por 3 segundos.
Duração da função: 3.0134708881378174
Dormirei por 5 segundos.
Duração da função: 5.014483690261841

Process finished with exit code 0
				
			

Perceba que a função calcula_tempo foi alterada e dentro dela foi definida uma função interna chamada wrapper que faz o trabalho de calcular o tempo de execução e chamar a função durma_xseg que é recebida como parâmetro através do parâmetro funcao_que_dorme. O ponto aqui é que a função calcula_tempo retorna para o chamador uma referência à função wrapper, o que pode ser interpretado como: “a função calcula_tempo retorna para o chamador o endereço de memória da execução da função wrapper”.

Na sequência do código foi atribuída à durma_1seg o retorno da função calcula_tempo, que como vimos é o endereço de memória da função interna wrapper.

Isto pode ser verificado imprimindo na tela o que é durma_1seg.

				
					print(durma_1seg)
				
			
				
					<function calcula_tempo.<locals>.wrapper at 0x0000027C0681A7A0>
				
			

Isto é, o conteúdo da variável durma_1seg é o endereço de memória da função interna wrapper.

Também podemos verificar o mesmo para todas as referências.

				
					durma_1seg = calcula_tempo(durma_1seg)
print('Endereço de memória da função wrapper fazendo referência a durma_1seg: {}'.format(durma_1seg))
durma_3seg = calcula_tempo(durma_3seg)
print('Endereço de memória da função wrapper fazendo referência a durma_3seg: {}'.format(durma_3seg))
durma_5seg = calcula_tempo(durma_5seg)
print('Endereço de memória da função wrapper fazendo referência a durma_5seg: {}'.format(durma_5seg))

				
			
				
					Endereço de memória da função wrapper fazendo referência a durma_1seg: <function calcula_tempo.<locals>.wrapper at 0x000001EB0FB4AA70>
Endereço de memória da função wrapper fazendo referência a durma_3seg: <function calcula_tempo.<locals>.wrapper at 0x000001EB0FB4AB00>
Endereço de memória da função wrapper fazendo referência a durma_5seg: <function calcula_tempo.<locals>.wrapper at 0x000001EB0FB4AB90>

				
			

Note que os endereços de memória para as funções wrapper são diferentes, isto é, cada uma das 3 vezes que ela foi definida, gerou uma alocação de memória diferente.

Seguindo no código tempos a linha durma_1seg(), executa a função que estava armazenada na variável durma_1seg.

				
					durma_1seg()
				
			

É importante notar a diferença entre durma_1seg() e durma_1seg, com e sem parênteses, sendo durma_1seg uma referência a uma função e durma_1seg() sendo a indicação para executar uma função. Aqui os parênteses estão fazendo a seguinte indicação: “execute a função que está no endereço de memória 0x000001EB0FB4AA70.”

Voltando ao código principal, o fluxo de execução seria assim:

				
					durma_1seg = calcula_tempo(durma_1seg)
				
			

→ Chame a função calcula_tempo passando como parâmetro uma referência a função durma_1seg.

→ Já no código da função calcula_tempo, aloque memória para a função wrapper que irá executar, considerando funcao_que_dorme=durma_1seg, isto é, “memorize” que esta execução irá considerar a função durma_1seg.

→ Retorne para o chamador a posição de memória da função wrapper que foi memorizada, isto é, retorne o closure.

Note que a função wrapper não é executada, mas é feita alocação de memória para ela considerando o valor passado em funcao_que_dorme.

Seguindo no código principal temos o seguinte fluxo:

				
					durma_1seg()
				
			

→ Execute a função que está sendo referenciada em durma_1seg (função wrapper que foi memorizada e considera funcao_que_dorme=durma_1seg).

→ No código da função wrapper, registre o tempo de início na variável tempo_inicio.

→ Execute a função que foi passada como parâmetro e memorizada (durma_1seg).

→ No código da função durma_1seg, imprima na tela a mensagem que dormirá por 1 segundo.

→ Durma por 1 segundo.

→ Retorne o código para o chamador (wrapper).

→ Registre o tempo final na variável tempo_fim.

→ Imprima na tela o tempo de execução da função.

→ Fim da execução.

O ponto importante a ser notado é que a função wrapper que foi executada considerou o parâmetro funcao_que_dorme=durma_1seg, isto é, o valor que foi memorizado na definição da função no passo anterior. Como vimos anteriormente o closure nada mais é do que uma função aninhada e as variáveis livres pertencentes ao escopo do qual a função aninhada faz parte.

Lindão, o código ainda funciona como deveria. Aqui foi usado a primeira forma de se aplicar um decorador a uma função.Como melhorar isto e tornar o código mais elegante?

Syntatic Sugar

A seguir está o código que usa a segunda maneira de se aplicar um decorator a uma função.

				
					import time

def calcula_tempo(funcao_que_dorme):
    def wrapper():
        tempo_inicio = time.time()
        funcao_que_dorme()
        tempo_fim = time.time()
        print("Duração da função: {}".format(tempo_fim - tempo_inicio))
    return wrapper

@calcula_tempo
def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

durma_1seg()
durma_3seg()
durma_5seg()

				
			

A saída seria:

				
					Dormirei por 1 segundo.
Duração da função: 1.0070223808288574
Dormirei por 3 segundos.
Dormirei por 5 segundos.

Process finished with exit code 0
				
			

O código da função wrapper continua o mesmo, mas antes da função durma_1seg foi adicionado @calcula_tempo.

@calcula_tempo substitui o código usado na primeira forma de aplicar um decorator – durma_1seg = calcula_tempo(durma_1seg). Isto é, ele informa o seguinte para a função durma_1seg: “quando você for chamada você será passada como referência para a função calcula_tempo, que por sua vez irá estender o comportamento do seu código adicionando o cálculo do tempo que você levou para executar.”

Para usar um decorator basta fazer uso do @calcula_tempo imediatamente antes da definição da função e no restante do código a função poderá ser invocada normalmente e ainda sim mudará seu comportamento graças a definição do decorator.

Perceba que na saída da execução do código somente a função durma_1seg teve seu tempo medido, pois o decorator @calcula_tempo só foi associado a esta função.

Para que todas as funções que dormem tenham seu tempo medido o código seria:

				
					import time

def calcula_tempo(funcao_que_dorme):
    def wrapper():
        tempo_inicio = time.time()
        funcao_que_dorme()
        tempo_fim = time.time()
        print("Duração da função: {}".format(tempo_fim - tempo_inicio))
    return wrapper

@calcula_tempo
def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

@calcula_tempo
def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

@calcula_tempo
def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

durma_1seg()
durma_3seg()
durma_5seg()

				
			
				
					Dormirei por 1 segundo.
Duração da função: 1.0137159824371338
Dormirei por 3 segundos.
Duração da função: 3.0094916820526123
Dormirei por 5 segundos.
Duração da função: 5.007404804229736
				
			
Reuso

Um Decorator é apenas uma função comum, assim, pode ser colocada em um arquivo separado e importada para dentro de todos os outros arquivos que desejem utilizar o Decorator.

Foi criado o arquivo decorators.py com o seguinte código.

				
					import time

def calcula_tempo(funcao_que_dorme):
    def wrapper():
        tempo_inicio = time.time()
        funcao_que_dorme()
        tempo_fim = time.time()
        print("Duração da função: {}".format(tempo_fim - tempo_inicio))
    return wrapper

				
			

O código principal importa o código de decorators.py

				
					from decorators import *

@calcula_tempo
def durma_1seg():
    print('Dormirei por 1 segundo.')
    time.sleep(1)

@calcula_tempo
def durma_3seg():
    print('Dormirei por 3 segundos.')
    time.sleep(3)

@calcula_tempo
def durma_5seg():
    print('Dormirei por 5 segundos.')
    time.sleep(5)

durma_1seg()
durma_3seg()
durma_5seg()
				
			

Desta forma, qualquer outro código pode importar decorators.py e medir o tempo de execução usando um decorator antes da definição das suas funções.

Funções decoradas que usam parâmetros

Até o momento vimos a utilização de decorators e funções decoradas que não utilizam parâmetros. Mas e se uma função decorada precisar usar parâmetros?

O código a seguir já foi utilizado como exemplo um uma seção anterior e a ele foi adicionada a função soma, que soma dois valores passados como argumentos.

				
					def verbose(func):
    def wrapper():
        print('Before', func.__name__)
        result = func()
        print('After', func.__name__)
        return result
    return wrapper

@verbose
def hello():
    print('Hello')

@verbose
def soma(a, b):
    print(a + b)

hello()
soma(1, 2)
				
			
				
					TypeError: verbose.<locals>.wrapper() takes 0 positional arguments but 2 were given
				
			

A mensagem de erro diz que a função wrapper não aceita argumentos, mas que dois foram passados.

A questão aqui é que a função hello não utiliza parâmetros, mas a função soma utiliza 2 parâmetros. Então, como fazer para que wrapper e func possam receber uma quantidade variável de argumentos?

A resposta é *args e **kwargs.

Para que a função soma, quando for executada dentro do escopo de wrapper, receba dois valores é preciso também definir que wrapper tenha dois parâmetros.

				
					def verbose(func):
    def wrapper(*args, **kwargs):
        print('Before', func.__name__)
        result = func(*args, **kwargs)
        print('After', func.__name__)
        return result
    return wrapper

@verbose
def hello():
    print('Hello')

@verbose
def soma(a, b):
    print(a + b)

hello()
soma(1, 2)
				
			
				
					Before hello
Hello
After hello
Before soma
3
After soma
				
			

A próxima seção explica o funcionamento de *args e **kwargs.

*args **kwargs

Python suporta 4 tipos de parâmetros para funções:

  1. Normal Parameters – tem um nome e uma posição.
  2. Keyword Parameters – tem um nome.
  3. Variable Parameters – precedidos por um *, tem uma posição.
  4. Keyword Variable Parameters – precedidos por **, tem um nome.

Normal parameters e Keyword parameters são que foram utilizados até aqui.

Variable Parameters (*args) permitem as funções receber um número arbitrário de argumentos. Dentro da função, “args” será uma tupla.

				
					def demo_args(*args):
    print(type(args))
    print(args)

demo_args(3, 'teste')
				
			
				
					<class 'tuple'>
(3, 'teste')
				
			

O asterístico (*) pode ser usado para decompor uma sequência.

				
					def demo_args(*args):
    print(type(args))
    print(args)
lista = ['Paulo', 'Ana']

demo_args(lista)
				
			
				
					<class 'tuple'>
(['Paulo', 'Ana'],)
				
			

No exemplo acima, foi passado como argumento uma lista com dois itens (Paulo e Ana). Como resultado, “args” é uma tupla com um item, que é a lista com dois itens.

Usando asterístico como operador de decomposição.

				
					def demo_args(*args):
    print(type(args))
    print(args)
lista = ['Paulo', 'Ana']

demo_args(*lista)
				
			
				
					<class 'tuple'>
('Paulo', 'Ana')
				
			

Aqui o asterístico (*) decompõe a lista antes de passar como argumentos, assim, “args” dentro da função é uma tupla com dois elementos.

O código abaixo seria o equivalente do código anterior, entretanto, passando os elementos da lista individualmente.

				
					def demo_args(*args):
    print(type(args))
    print(args)
lista = ['Paulo', 'Ana']

demo_args(lista[0], lista[1])
				
			
				
					<class 'tuple'>
('Paulo', 'Ana')
				
			

Keyword variable parameters (**kwargs) funcionam como  variable parameters (*args), isto é, quando usado em uma função indica que ela pode receber qualquer número de argumentos keyword. Dentro da função **kwargs é um dicionário.

				
					def demo_kwargs(**kwargs):
    print(type(kwargs))
    print(kwargs)

demo_kwargs(a=1, b=2)
				
			
				
					<class 'dict'>
{'a': 1, 'b': 2}
				
			

No exemplo abaixo o operador “**” será utilizado para decompor um dicionário antes de passar como argumento para a função demo_kwargs.

				
					def demo_kwargs(**kwargs):
    print(type(kwargs))
    print(kwargs)

meu_dict = {'a':1, 'b':2}

demo_kwargs(**meu_dict)
				
			
				
					<class 'dict'>
{'a': 1, 'b': 2}
				
			

Uma função pode ter os 4 tipos de parâmetros.

				
					def demo_params(normal, kw='teste', *args, **kwargs):
    print(normal)
    print(kw)
    print(args)
    print(kwargs)

lista = ['Paulo', 'Ana']
meu_dict = {'a':1, 'b':2}

demo_params('normal', 'kw', *lista, **meu_dict)
				
			
				
					normal
kw
('Paulo', 'Ana')
{'a': 1, 'b': 2}

				
			
Decorator com parâmetros

Vamos supor que tenhamos uma função decorada, onde o objetivo do decorator seja truncar uma string até o terceiro caractere.

				
					def trunc(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[:3]
    return wrapper

@trunc
def data():
    return "algumacoisa"

print(data())
				
			
				
					alg
				
			

Mas imagine que ao invés de truncar a string com o valor fixo 3 desejamos informar ao decorator o número de caracteres usar.

Se usarmos a primeira forma de aplicar um decorator poderíamos ter o seguinte código.

				
					def trunc(func, x):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[:x]
    return wrapper

def data():
    return "algumacoisa"

data = trunc(data,7)

print(data())
				
			
				
					algumac
				
			

Perceba que a função trunc agora recebe o parâmetro x e este pode ser utilizado dentro do escopo a função wrapper aninhada.

A principal modificação em relação ao código anterior é que aqui usamos a primeira forma para aplicar o decorator, isto é, usamos data = trunc(data,7) ao invés de @trunc(7).

Vamos tentar usar a segunda forma para aplicar um decorator.

				
					def trunc(func, x):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[:x]
    return wrapper

@trunc(7)
def data():
    return "algumacoisa"

print(data())
				
			
				
					TypeError: trunc() missing 1 required positional argument: 'x'
				
			

O erro informa que está faltando um argumento na chamada de trunc.

Isto mostra que quando usamos a segunda forma de aplicar um decorator (@trunc) sem passar nenhum parâmetro, o Python internamente entende que quando a função decorada for chamada ela deve ser passada como parâmetro para o decorator, mas que se @trunc tiver algum argumento definido, então, todos os argumentos que serão recebidos por trunc(decorator) devem estar presentes. Isto nos leva a outro problema pois tentar passar a função data como argumento em @trunc também gera um erro.

				
					def trunc(func, x):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result[:x]
    return wrapper

@trunc(data,7)
def data():
    return "algumacoisa"

print(data())
				
			
				
					NameError: name 'data' is not defined
				
			

Na verdade a PEP 318 informa que na segunda forma de aplicar um decorator não devem ser usados com parênteses quando estiver encapsulando uma função. 

Até o momento temos usado um closure para gerar uma nova função. Então, o método para gerar um decorator parametrizável é encapsulá-lo em outra função. O próprio decorator parametrizado é na verdade um gerador de decorador.

Então, para contornar este problema devemos utilizar @ para invocar uma outra função que irá ser um gerador de decorator.

				
					def limit(x):
    def trunc(func):
        def wrapper(*args, **kwargs):
            # x = 3
            result = func(*args, **kwargs)
            return result[:x]
        return wrapper
    return trunc

@limit(7)
def data():
    return "algumacoisa"

print(data())

				
			
				
					algumac
				
			

It’s done!

Bruno Veiga

Bruno Veiga

Arquiteto Cloud e Arquiteto de Soluções. Me dedicando em compartilhar conhecimento e ajudar empresas a encontrar as melhores soluções tecnológicas para os problemas do negócio com agilidade, segurança, equipes alinhadas e dentro do orçamento.