Os decoradores em Python são funcionalidades fascinantes e poderosas que podem melhorar o seu código. Eles permitem modificar ou estender o comportamento de funções e métodos sem alterar o código original. Ao utilizá-los, é possível adicionar recursos a funções existentes de maneira fluida e sustentável, tornando-o mais legível e eficiente.
Neste artigo, exploraremos:
- Como as funções funcionam em Python;
- O conceito de decoradores;
- Como os decoradores funcionam em Python;
- Exemplos práticos para ilustrar a utilidade dos decoradores.
Seja você um desenvolvedor experiente em Python ou alguém que está apenas começando, compreender os decoradores pode abrir novas possibilidades no seu conjunto de habilidades de programação.
Vamos começar!
Como as funções funcionam em Python
Em Python, as funções são objetos de primeira classe. Isso significa que você pode trabalhar com elas da mesma forma que trabalha com outros objetos, como strings, números inteiros ou listas. O fato de que são objetos de primeira classe, confere às suas funções propriedades poderosas:
- Uma função é uma instância do tipo Object;
- Pode ser armazenada em uma variável;
- Pode ser passada como parâmetro para outra função;
- Pode ser retornada de outra função;
- Pode ser armazenada em estruturas de dados como listas ou dicionários.
Para entender melhor as funções, vamos criar alguns exemplos:
def add_numbers(number1, number2):
return number1 + number2
def subtract_numbers(number1, number2):
return number1 - number2
def apply_function(operation_func):
return operation_func(10, 5)
print(f"Addition: {apply_function(add_numbers)}")
print(f"Subtraction: {apply_function(subtract_numbers)}")
Executando o script Python que criamos, teremos o seguinte resultado:
Addition: 15
Subtraction: 5
Neste exemplo, criamos duas funções regulares, add_numbers
e subtract_numbers
, que recebem dois números como entrada. Também criamos outra função, apply_function
, que aceita uma função como argumento. Em seguida, usamos apply_function
duas vezes — primeiro com add_numbers
e depois com subtract_numbers
.
Aqui está uma distinção importante a ser notada. Quando passamos add_numbers
para apply_function
, usamos add_numbers
sem parênteses. Isso significa que estamos passando uma referência para a própria função, e não a chamando diretamente. Por outro lado, quando escrevemos apply_function(...)
com parênteses, estamos chamando ativamente a função, e ela é executada como de costume.
Isso demonstra o conceito de funções como objetos de primeira classe. Uma função sem parênteses é apenas uma referência, como uma variável. Mas, quando você adiciona parênteses, ela é executada, retornando seu resultado.
Função interna: definindo funções dentro de outras
Também é possível definir funções dentro de outras funções, o que é chamado de inner-functions (funções internas), e logo veremos como isso é relevante para a criação de decoradores em Python.
Vamos criar um novo exemplo com duas funções internas:
def greetings(name):
def hello():
return "Hello"
def say_something_nice():
return "Have a nice day!"
return f"{hello()} {name}. {say_something_nice()}"
print(greetings("John"))
Ao executar isso, você receberá o seguinte output:
Hello John. Have a nice day!
Dentro da função greetings, criamos outras duas funções — hello
e say_something_nice
. Elas são chamadas de funções internas porque são definidas dentro de outra função.
É importante entender que funções internas têm escopo local à sua função principal. Isso significa que você não pode chamar uma função interna diretamente, a menos que a principal já tenha sido chamada. Se você tentar executar hello
fora de greetings
, receberá um erro.
Aqui está um exemplo em que modificamos o código para chamar hello
fora da função greetings
para ver o que acontece.
def greetings(name):
def hello():
return "Hello"
def say_something_nice():
return "Have a nice day!"
return f"{hello()} {name}. {say_something_nice()}"
print(greetings("John"))
print(hello())
Agora, ao executar, você receberá o output:
Hello John. Have a nice day!
Traceback (most recent call last):
. . .
NameError: name 'hello' is not defined
Você pode perceber que a função é executada corretamente quando você chama a principal (greetings), mas se tentar executar apenas a função interna hello, receberá um erro. Isso acontece devido ao escopo local da função interna, que não está disponível fora da principal.
Funções que retornam outras funções
As funções também podem retornar a outras funções. Aqui está um exemplo mais claro:
def classify_number(number):
def even():
return "Number is even"
def odd():
return "Number is odd"
if number % 2 == 0:
return even
else:
return odd
number = 10
result = classify_number(number)
print(f"Return from classify_number: {result}")
print(f"Executing the function returned by classify_number: {result()}")
print("\n")
number = 11
result = classify_number(number)
print(f"Return from classify_number: {result}")
print(f"Executing the function returned by classify_number: {result()}")
Ao executar este código, o output será:
Return from classify_number: <function classify_number.<locals>.even at 0x104beb040>
Executing the function returned by classify_number: Number is even
Return from classify_number: <function classify_number.<locals>.odd at 0x104beb160>
Executing the function returned by classify_number: Number is odd
Explicação
- A função
classify_number
contém duas funções internas:even
eodd
, que retornam retornam um objeto do tipo texto (string) que descrevem o tipo de número. - Dependendo se o número de entrada é par ou ímpar, a função
classify_number
retorna uma referência para a funçãoeven
ouodd
. Ela ainda não as executa — apenas fornece uma referência. - Uma vez que a função retornada (
result
) é armazenada, você pode chamá-la mais tarde, comoresult()
, para executá-la e obter a mensagem apropriada.
Ao retornar uma referência para a função, você tem a flexibilidade de chamá-la quando precisar, em vez de obter o resultado imediatamente durante a chamada inicial da função.
Iniciando com decoradores em Python
Agora que entendemos como as funções funcionam em Python, vamos aprender sobre decoradores e por que eles são tão poderosos. Um decorador é essencialmente uma função que modifica o comportamento de outra função.
Aqui está um exemplo simples:
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} is called")
func(*args, **kwargs)
print(f"Function {func.__name__} is completed")
return wrapper
def add_numbers(number1, number2):
print(number1 + number2)
add_numbers = decorator(add_numbers)
Explicação
- Criamos duas funções:
decorator
: função decoradora.add_numbers
: função que imprime a soma de dois números.
- Dentro do
decorator
, definimos uma função interna chamadawrapper
.- Esta função recebe quaisquer argumentos (
*args
) e argumentos nomeados (**kwargs
), que podem ser passados para a função original. - Ela imprime uma mensagem antes e depois de chamar a função original.
- Esta função recebe quaisquer argumentos (
- Aplicamos o decorador à
add_numbers
ao reatribuiradd_numbers
para o resultado dodecorator(add_numbers)
. Agora,add_numbers
aponta para a funçãowrapper
.
Quando executado em um shell Python:
>>> from decorator1 import add_numbers
>>> add_numbers(10, 5)
Isso vai produzir:
Function add_numbers is called
15
Function add_numbers is completed
O que está acontecendo
- A função
add_numbers
agora se refere à funçãowrapper
. - A
wrapper
, por sua vez, chama a função originaladd_numbers
(a que definimos inicialmente) e adiciona a funcionalidade extra (imprimir mensagens) antes e depois de sua execução.
>>> from decorator1 import add_numbers
>>> add_numbers
<function decorator.<locals>.wrapper at 0x104f6c700>
Você sabe que um decorador basicamente envolve uma função, modificando seu comportamento. Mas a forma como aplicamos o decorador no exemplo anterior é peculiar. Abaixo está uma alternativa mais adequada:
A forma Pythonica de usar decoradores
A forma como aplicamos o decorador acima funciona, mas existe uma abordagem mais limpa e Pythonica usando o símbolo @ (chamada de “pie syntax”). Usando isso, nosso exemplo fica assim:
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} is called")
func(*args, **kwargs)
print(f"Function {func.__name__} is completed")
return wrapper
@decorator
def add_numbers(number1, number2):
print(number1 + number2)
Aqui está o que mudou:
- Ao adicionar
@decorator
acima da funçãoadd_numbers
, aplicamos automaticamente odecorator
sem precisar reatribuiradd_numbers
manualmente. - O comportamento continua o mesmo, mas o código fica muito mais limpo e fácil de ler.
No geral, isso demonstra como os decoradores permitem envolver uma função com um comportamento adicional de forma elegante e reutilizável
Exemplos do mundo real de decoradores em Python
Agora que sabemos como criar decoradores, vamos ver alguns exemplos práticos para entender como eles podem ser úteis em cenários do mundo real.
Exemplo 1: medindo o tempo de execução
Um uso comum de decoradores é medir quanto tempo uma função leva para ser executada. Isso pode ser especialmente útil para otimização de desempenho. Aqui está como podemos criar esse decorador:
import time
def execution_timer(func):
"""Print the runtime of the decorated function"""
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__}() in {run_time:.4f} secs")
return value
return wrapper_timer
Agora, vamos usar esse decorador com uma função que leva tempo para ser executada e verificar o output:
>>> from execution_timer_decorator import execution_timer
>>> @execution_timer
... def amazing_function():
... for _ in range(500):
... sum([number*number for number in range(10000)])
...
>>> amazing_function()
Finished amazing_function() in 0.1985 secs
Como funciona
- O decorador armazena o horário de início (
start_time
) logo antes da execução da função. - Após a função terminar, ele calcula o tempo total de execução (
run_time
) subtraindo ostart_time
do horário de término. - Por fim, ele imprime o tempo de execução no console do terminal.
Isso pode ajudá-lo a identificar funções lentas no seu código.
Exemplo 2: verificando cabeçalhos em uma view do Django
Outro decorador útil pode verificar se uma requisição contém cabeçalhos específicos antes que o endpoint a processe. Por exemplo, você pode querer garantir que uma requisição contenha determinados cabeçalhos antes de aceitá-la.
Aqui está como você pode criar esse decorador:
import logging
from functools import wraps
from typing import List
from rest_framework import status
from rest_framework.response import Response
def request_has_headers(headers_names: List[str]):
def decorator(function):
@wraps(function)
def wrapper(request, *args, **kwargs):
for header in headers_names:
if header.capitalize() not in request.headers:
return Response(status=status.HTTP_400_BAD_REQUEST, data=f"Header {header} is missing")
return function(request, *args, **kwargs)
return wrapper
return decorator
Usando o decorador em uma View do Django
Agora você pode usar esse decorador para verificar os cabeçalhos sem sobrecarregar a lógica da sua view:
@method_decorator(request_has_headers(["my-first-header", “my-second-header]))
def list(self, request):
my_data = MyModel.objects.all()
return Response(self.serializer_class(my_data, many=True, context={"request": request}).data, status=status.HTTP_200_OK)
Como Funciona
- Passe uma lista de cabeçalhos necessários (
["my-first-header", "my-second-header"]
) para o decorador. - O decorador percorre os cabeçalhos necessários e verifica se cada um está presente nos cabeçalhos da requisição.
- Se um cabeçalho estiver ausente, ele retorna imediatamente uma resposta
400 Bad Request
com mensagem de erro. - Se todos os cabeçalhos estiverem presentes, a função original (por exemplo, a view do Django) é executada.
Essa abordagem pode ajudar a garantir que suas requisições atendam a requisitos específicos, sem sobrecarregar a view com verificações repetitivas.
Conclusão
Neste artigo, exploramos como as funções em Python podem ser passadas como outros objetos, como definir funções internas e como usar e criar decoradores. Decoradores são um recurso poderoso e flexível em Python que permite modificar o comportamento de funções ou métodos sem alterar seu código-fonte. Eles facilitam a adição de funcionalidades reutilizáveis, como medir o tempo de execução ou validar requisições, enquanto mantêm seu código limpo e organizado. No geral, é uma ferramenta incrivelmente útil no desenvolvimento em Python.