Introdução
Neste laboratório, você aprenderá sobre decorators (decoradores) em Python, um recurso poderoso que pode modificar o comportamento de funções e métodos. Decorators são comumente usados para tarefas como logging (registro), medição de desempenho, controle de acesso e verificação de tipos.
Você aprenderá como encadear múltiplos decorators, criar decorators que aceitam parâmetros, preservar metadados de funções ao usar decorators e aplicar decorators a diferentes tipos de métodos de classe. Os arquivos com os quais você trabalhará são logcall.py, validate.py e sample.py.
Preservando Metadados de Funções em Decoradores
Em Python, decorators (decoradores) são uma ferramenta poderosa que permite modificar o comportamento de funções. No entanto, ao usar um decorator para envolver uma função, há um pequeno problema. Por padrão, os metadados da função original, como seu nome, docstring (string de documentação) e anotações, são perdidos. Metadados são importantes porque ajudam na introspecção (examinando a estrutura do código) e na geração de documentação. Vamos primeiro verificar esse problema.
Abra seu terminal no WebIDE. Executaremos alguns comandos Python para ver o que acontece quando usamos um decorator. Os seguintes comandos criarão uma função simples add envolvida com um decorator e, em seguida, imprimirão a função e sua docstring.
cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
Ao executar esses comandos, você verá uma saída semelhante a esta:
<function wrapper at 0x...>
None
Observe que, em vez de mostrar o nome da função como add, ele mostra wrapper. E a docstring, que deveria ser 'Adds two things', é None. Isso pode ser um grande problema quando você está usando ferramentas que dependem desses metadados, como ferramentas de introspecção ou geradores de documentação.
Corrigindo o Problema com functools.wraps
O módulo functools do Python vem para o resgate. Ele fornece um decorator wraps que pode nos ajudar a preservar os metadados da função. Vamos ver como podemos modificar nosso decorator logged para usar wraps.
- Primeiro, abra o arquivo
logcall.pyno WebIDE. Você pode navegar até o diretório do projeto usando o seguinte comando no terminal:
cd ~/project
- Agora, atualize o decorator
loggedemlogcall.pycom o seguinte código. O decorator@wraps(func)é a chave aqui. Ele copia todos os metadados da função originalfuncpara a função wrapper.
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
O decorator
@wraps(func)faz um trabalho importante. Ele pega todos os metadados (como o nome, docstring e anotações) da função originalfunce os anexa à funçãowrapper. Dessa forma, quando usamos a função decorada, ela terá os metadados corretos.Vamos testar nosso decorator aprimorado. Execute os seguintes comandos no terminal:
python3 -c "from logcall import logged; @logged
def add(x,y):
'Adds two things'
return x+y
print(add)
print(add.__doc__)"
Agora você deve ver:
<function add at 0x...>
Adds two things
Ótimo! O nome da função e a docstring são preservados. Isso significa que nosso decorator agora está funcionando como esperado, e os metadados da função original estão intactos.
Corrigindo o Decorator validate.py
Agora, vamos aplicar a mesma correção ao decorator validated em validate.py. Este decorator é usado para validar os tipos de argumentos de função e o valor de retorno com base nas anotações da função.
Abra
validate.pyno WebIDE.Atualize o decorator
validatedcom o decorator@wraps. O seguinte código mostra como fazer isso. O decorator@wraps(func)é adicionado à funçãowrapperdentro do decoratorvalidatedpara preservar os metadados.
from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
- Vamos testar se nosso decorator
validatedagora preserva metadados. Execute os seguintes comandos no terminal:
python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
'Multiplies two integers'
return x * y
print(multiply)
print(multiply.__doc__)"
Você deve ver:
<function multiply at 0......>
Multiplies two integers
Agora, ambos os decorators, logged e validated, preservam adequadamente os metadados das funções que decoram. Isso garante que, ao usar esses decorators, as funções ainda terão seus nomes originais, docstrings e anotações, o que é muito útil para a legibilidade e manutenibilidade do código.
Criando Decoradores com Argumentos
Até agora, temos usado o decorator @logged, que sempre imprime uma mensagem fixa. Mas e se você quiser personalizar o formato da mensagem? Nesta seção, aprenderemos como criar um novo decorator que pode aceitar argumentos, dando a você mais flexibilidade na forma como você usa decorators.
Entendendo Decoradores Parametrizados
Um decorator parametrizado é um tipo especial de função. Em vez de modificar diretamente outra função, ele retorna um decorator. A estrutura geral de um decorator parametrizado se parece com isto:
def decorator_with_args(arg1, arg2, ...):
def actual_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Use arg1, arg2, ... here
## Call the original function
return func(*args, **kwargs)
return wrapper
return actual_decorator
Quando você usa @decorator_with_args(value1, value2) em seu código, o Python primeiro chama decorator_with_args(value1, value2). Essa chamada retorna o decorator real, que é então aplicado à função que segue a sintaxe @. Esse processo de duas etapas é fundamental para o funcionamento dos decorators parametrizados.
Criando o Decorator logformat
Vamos criar um decorator @logformat(fmt) que recebe uma string de formato como argumento. Isso nos permitirá personalizar a mensagem de logging.
- Abra
logcall.pyno WebIDE e adicione o novo decorator. O código abaixo mostra como definir tanto o decoratorloggedexistente quanto o novo decoratorlogformat:
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
No decorator logformat, a função externa logformat recebe uma string de formato fmt como argumento. Em seguida, ele retorna a função decorator, que é o decorator real que modifica a função de destino.
- Agora, vamos testar nosso novo decorator modificando
sample.py. O código a seguir mostra como usar os decoratorsloggedelogformatem diferentes funções:
from logcall import logged, logformat
@logged
def add(x, y):
"Adds two numbers"
return x + y
@logged
def sub(x, y):
"Subtracts y from x"
return x - y
@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
"Multiplies two numbers"
return x * y
Aqui, as funções add e sub usam o decorator logged, enquanto a função mul usa o decorator logformat com uma string de formato personalizada.
- Execute o
sample.pyatualizado para ver os resultados. Abra seu terminal e execute o seguinte comando:
cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"
Você deve ver uma saída semelhante a:
Calling add
5
sample.py:mul
6
Essa saída mostra que o decorator logged imprime o nome da função como esperado, e o decorator logformat usa a string de formato personalizada para imprimir o nome do arquivo e o nome da função.
Redefinindo o Decorator logged Usando logformat
Agora que temos um decorator logformat mais flexível, podemos redefinir nosso decorator logged original usando-o. Isso nos ajudará a reutilizar o código e manter um formato de logging consistente.
- Atualize
logcall.pycom o seguinte código:
from functools import wraps
def logformat(fmt):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(fmt.format(func=func))
return func(*args, **kwargs)
return wrapper
return decorator
## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)
Aqui, usamos uma função lambda para definir o decorator logged em termos do decorator logformat. A função lambda recebe uma função func e aplica o decorator logformat com uma string de formato específica.
- Teste se o decorator
loggedredefinido ainda funciona. Abra seu terminal e execute o seguinte comando:
cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
return f'Hello, {name}'
print(greet('World'))"
Você deve ver:
Calling greet
Hello, World
Isso mostra que o decorator logged redefinido funciona como esperado, e reutilizamos com sucesso o decorator logformat para obter um formato de logging consistente.
Aplicando Decoradores a Métodos de Classe
Agora, vamos explorar como os decorators interagem com métodos de classe. Isso pode ser um pouco complicado porque o Python tem diferentes tipos de métodos: métodos de instância, métodos de classe, métodos estáticos e propriedades. Decorators são funções que recebem outra função e estendem o comportamento da última função sem modificá-la explicitamente. Ao aplicar decorators a métodos de classe, precisamos prestar atenção em como eles funcionam com esses diferentes tipos de métodos.
Entendendo o Desafio
Vamos ver o que acontece quando aplicamos nosso decorator @logged a diferentes tipos de métodos. O decorator @logged provavelmente é usado para registrar informações sobre as chamadas de método.
- Crie um novo arquivo
methods.pyno WebIDE. Este arquivo conterá nossa classe com diferentes tipos de métodos decorados com o decorator@logged.
from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@logged
@classmethod
def class_method(cls):
print("Class method called")
return "class result"
@logged
@staticmethod
def static_method():
print("Static method called")
return "static result"
@logged
@property
def property_method(self):
print("Property method called")
return "property result"
Neste código, temos uma classe Spam com quatro tipos diferentes de métodos. Cada método é decorado com o decorator @logged, e alguns também são decorados com outros decorators embutidos como @classmethod, @staticmethod e @property.
- Vamos testar como funciona. Executaremos um comando Python no terminal para chamar esses métodos e ver a saída.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
Ao executar este comando, você pode notar alguns problemas:
- O decorator
@propertypode não funcionar corretamente com nosso decorator@logged. O decorator@propertyé usado para definir um método como uma propriedade, e ele tem uma maneira específica de funcionar. Quando combinado com o decorator@logged, pode haver conflitos. - A ordem dos decorators importa para
@classmethode@staticmethod. A ordem em que os decorators são aplicados pode alterar o comportamento do método.
A Ordem dos Decoradores
Ao aplicar vários decorators, eles são aplicados de baixo para cima. Isso significa que o decorator mais próximo da definição do método é aplicado primeiro, e então os acima dele são aplicados em sequência. Por exemplo:
@decorator1
@decorator2
def func():
pass
Isso é equivalente a:
func = decorator1(decorator2(func))
Neste exemplo, decorator2 é aplicado a func primeiro, e então decorator1 é aplicado ao resultado de decorator2(func).
Corrigindo a Ordem do Decorator
Vamos atualizar nosso arquivo methods.py para corrigir a ordem do decorator. Ao alterar a ordem dos decorators, podemos garantir que cada método funcione como esperado.
from logcall import logged
class Spam:
@logged
def instance_method(self):
print("Instance method called")
return "instance result"
@classmethod
@logged
def class_method(cls):
print("Class method called")
return "class result"
@staticmethod
@logged
def static_method():
print("Static method called")
return "static result"
@property
@logged
def property_method(self):
print("Property method called")
return "property result"
Nesta versão atualizada:
- Para
instance_method, a ordem não importa. Métodos de instância são chamados em uma instância da classe, e o decorator@loggedpode ser aplicado em qualquer ordem sem afetar sua funcionalidade básica. - Para
class_method, aplicamos@classmethodapós@logged. O decorator@classmethodaltera a maneira como o método é chamado, e aplicá-lo após@loggedgarante que o logging funcione corretamente. - Para
static_method, aplicamos@staticmethodapós@logged. Semelhante ao@classmethod, o decorator@staticmethodtem seu próprio comportamento, e a ordem com o decorator@loggedprecisa estar correta. - Para
property_method, aplicamos@propertyapós@logged. Isso garante que o comportamento da propriedade seja mantido, além de obter a funcionalidade de logging.
- Vamos testar o código atualizado. Executaremos o mesmo comando que antes para ver se os problemas foram corrigidos.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"
Você deve agora ver o logging adequado para todos os tipos de métodos:
Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result
Melhores Práticas para Decoradores de Métodos
Ao trabalhar com decorators de métodos, siga estas melhores práticas:
- Aplique decorators de transformação de método (
@classmethod,@staticmethod,@property) após seus decorators personalizados. Isso garante que os decorators personalizados possam executar seu logging ou outras operações primeiro, e então os decorators embutidos podem transformar o método conforme o pretendido. - Esteja ciente de que a execução do decorator ocorre no momento da definição da classe, não no momento da chamada do método. Isso significa que qualquer código de configuração ou inicialização no decorator será executado quando a classe for definida, não quando o método for chamado.
- Para casos mais complexos, você pode precisar criar decorators especializados para diferentes tipos de métodos. Diferentes tipos de métodos têm comportamentos diferentes, e um decorator único pode não funcionar em todas as situações.
Criando um Decorator de Aplicação de Tipos com Argumentos
Nos passos anteriores, aprendemos sobre o decorator @validated. Este decorator é usado para impor anotações de tipo em funções Python. Anotações de tipo são uma maneira de especificar os tipos esperados de argumentos de função e valores de retorno. Agora, vamos dar um passo adiante. Criaremos um decorator mais flexível que pode aceitar especificações de tipo como argumentos. Isso significa que podemos definir os tipos que queremos para cada argumento e o valor de retorno de uma forma mais explícita.
Entendendo o Objetivo
Nosso objetivo é criar um decorator @enforce(). Este decorator nos permitirá especificar restrições de tipo usando argumentos de palavra-chave. Aqui está um exemplo de como ele funcionará:
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
Neste exemplo, estamos usando o decorator @enforce para especificar que os argumentos x e y da função add devem ser do tipo Integer, e o valor de retorno também deve ser do tipo Integer. Este decorator se comportará de maneira semelhante ao nosso decorator @validated anterior, mas nos dá mais controle sobre as especificações de tipo.
Criando o Decorator enforce
- Primeiro, abra o arquivo
validate.pyno WebIDE. Adicionaremos nosso novo decorator a este arquivo. Aqui está o código que adicionaremos:
from functools import wraps
class Integer:
@classmethod
def __instancecheck__(cls, x):
return isinstance(x, int)
def validated(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Get function annotations
annotations = func.__annotations__
## Check arguments against annotations
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return' in annotations and not isinstance(result, annotations['return']):
raise TypeError(f'Expected return value to be {annotations["return"].__name__}')
return result
return wrapper
def enforce(**type_specs):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
## Check argument types
for arg_name, arg_value in zip(func.__code__.co_varnames, args):
if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')
## Run the function and get the result
result = func(*args, **kwargs)
## Check the return value
if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')
return result
return wrapper
return decorator
Vamos detalhar o que este código faz. A classe Integer é usada para definir um tipo personalizado. O decorator validated verifica os tipos de argumentos de função e o valor de retorno com base nas anotações de tipo da função. O decorator enforce é o novo que estamos criando. Ele recebe argumentos de palavra-chave que especificam os tipos para cada argumento e o valor de retorno. Dentro da função wrapper do decorator enforce, verificamos se os tipos dos argumentos e o valor de retorno correspondem aos tipos especificados. Caso contrário, lançamos um TypeError.
- Agora, vamos testar nosso novo decorator
@enforce. Executaremos alguns casos de teste para ver se ele funciona como esperado. Aqui está o código para executar os testes:
cd ~/project
python3 -c "from validate import enforce, Integer
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
return x + y
## This should work
print(add(2, 3))
## This should raise a TypeError
try:
print(add('2', 3))
except TypeError as e:
print(f'Error: {e}')
## This should raise a TypeError
try:
@enforce(x=Integer, y=Integer, return_=Integer)
def bad_add(x, y):
return str(x + y)
print(bad_add(2, 3))
except TypeError as e:
print(f'Error: {e}')"
Neste código de teste, primeiro definimos uma função add com o decorator @enforce. Em seguida, chamamos a função add com argumentos válidos, que devem funcionar sem erros. Em seguida, chamamos a função add com um argumento inválido, que deve lançar um TypeError. Finalmente, definimos uma função bad_add que retorna um valor do tipo errado, que também deve lançar um TypeError.
Ao executar este código de teste, você deve ver uma saída semelhante à seguinte:
5
Error: Expected x to be Integer
Error: Expected return value to be Integer
Esta saída mostra que nosso decorator @enforce está funcionando corretamente. Ele lança um TypeError quando os tipos dos argumentos ou o valor de retorno não correspondem aos tipos especificados.
Comparando as Duas Abordagens
Tanto o decorator @validated quanto o @enforce atingem o mesmo objetivo de impor restrições de tipo, mas o fazem de maneiras diferentes.
O decorator
@validatedusa as anotações de tipo embutidas do Python. Aqui está um exemplo:@validated def add(x: Integer, y: Integer) -> Integer: return x + yCom esta abordagem, especificamos os tipos diretamente na definição da função usando anotações de tipo. Este é um recurso embutido do Python, e ele fornece melhor suporte em Ambientes de Desenvolvimento Integrados (IDEs). Os IDEs podem usar essas anotações de tipo para fornecer preenchimento de código, verificação de tipo e outros recursos úteis.
O decorator
@enforce, por outro lado, usa argumentos de palavra-chave para especificar os tipos. Aqui está um exemplo:@enforce(x=Integer, y=Integer, return_=Integer) def add(x, y): return x + yEsta abordagem é mais explícita porque estamos passando diretamente as especificações de tipo como argumentos para o decorator. Pode ser útil ao trabalhar com bibliotecas que dependem de outros sistemas de anotação.
Cada abordagem tem suas próprias vantagens. As anotações de tipo são uma parte nativa do Python e oferecem melhor suporte de IDE, enquanto a abordagem @enforce nos dá mais flexibilidade e explicitude. Você pode escolher a abordagem que melhor se adapta às suas necessidades, dependendo do projeto em que está trabalhando.
Resumo
Neste laboratório, você aprendeu a criar e usar decorators de forma eficaz. Você aprendeu a preservar metadados de função com functools.wraps, criar decorators que aceitam parâmetros, lidar com múltiplos decorators e entender sua ordem de aplicação. Você também aprendeu a aplicar decorators a diferentes métodos de classe e a criar um decorator de aplicação de tipos que recebe argumentos.
Esses padrões de decorator são comumente usados em frameworks Python como Flask, Django e pytest. Dominar decorators permitirá que você escreva um código mais sustentável e reutilizável. Para aprofundar seu aprendizado, você pode explorar gerenciadores de contexto, decorators baseados em classe, usando decorators para caching e verificação de tipo avançada com decorators.