Definir Funções Decoradoras Simples

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Neste laboratório, você aprenderá o que são decoradores e como eles funcionam em Python. Decoradores são um recurso poderoso que permite modificar o comportamento de uma função sem alterar o código-fonte, e são amplamente utilizados em frameworks e bibliotecas Python.

Você também aprenderá a criar um decorador de logging (registro) simples e a implementar um mais complexo para validação de funções. Os arquivos envolvidos neste laboratório incluem logcall.py, sample.py e validate.py, sendo que validate.py será modificado.

Criando Seu Primeiro Decorador

O que são Decoradores?

Em Python, decoradores são uma sintaxe especial que pode ser bastante útil para iniciantes. Eles permitem que você modifique o comportamento de funções ou métodos. Pense em um decorador como uma função que recebe outra função como entrada. Em seguida, ele retorna uma nova função. Essa nova função geralmente estende ou altera o comportamento da função original.

Decoradores são aplicados usando o símbolo @. Você coloca este símbolo seguido pelo nome do decorador diretamente acima da definição de uma função. Esta é uma maneira simples de dizer ao Python que você deseja usar o decorador naquela função específica.

Criando um Decorador de Logging Simples

Vamos criar um decorador simples que registra informações quando uma função é chamada. Logging (registro) é uma tarefa comum em aplicações do mundo real, e usar um decorador para isso é uma ótima maneira de entender como eles funcionam.

  1. Primeiro, abra o editor VSCode. No diretório /home/labex/project, crie um novo arquivo chamado logcall.py. Este arquivo conterá nossa função decoradora.

  2. Adicione o seguinte código a logcall.py:

## logcall.py

def logged(func):
    print('Adding logging to', func.__name__)
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Vamos detalhar o que este código faz:

  • A função logged é nosso decorador. Ele recebe outra função, que chamamos de func, como um argumento. Esta func é a função à qual queremos adicionar logging.
  • Quando o decorador é aplicado a uma função, ele imprime uma mensagem. Esta mensagem nos diz que o logging está sendo adicionado à função com o nome fornecido.
  • Dentro da função logged, definimos uma função interna chamada wrapper. Esta função wrapper é o que substituirá a função original.
    • Quando a função decorada é chamada, a função wrapper imprime uma mensagem dizendo que a função está sendo chamada.
    • Em seguida, ele chama a função original (func) com todos os argumentos que foram passados para ela. Os *args e **kwargs são usados para aceitar qualquer número de argumentos posicionais e de palavras-chave.
    • Finalmente, ele retorna o resultado da função original.
  • A função logged retorna a função wrapper. Esta função wrapper agora será usada em vez da função original, adicionando a funcionalidade de logging.

Usando o Decorador

  1. Agora, no mesmo diretório (/home/labex/project), crie outro arquivo chamado sample.py com o seguinte código:
## sample.py

from logcall import logged

@logged
def add(x, y):
    return x + y

@logged
def sub(x, y):
    return x - y

A sintaxe @logged é muito importante aqui. Ela diz ao Python para aplicar o decorador logged às funções add e sub. Portanto, sempre que essas funções forem chamadas, a funcionalidade de logging adicionada pelo decorador será executada.

Testando o Decorador

  1. Para testar seu decorador, abra um terminal no VSCode. Primeiro, altere o diretório para o diretório do projeto usando o seguinte comando:
cd /home/labex/project

Em seguida, inicie o interpretador Python:

python3
  1. No interpretador Python, importe o módulo sample e teste as funções decoradas:
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()

Observe que, ao importar o módulo sample, as mensagens "Adding logging to..." são impressas. Isso ocorre porque o decorador é aplicado quando o módulo é importado. Cada vez que você chama uma das funções decoradas, a mensagem "Calling..." é impressa. Isso mostra que o decorador está funcionando como esperado.

Este decorador simples demonstra o conceito básico de decoradores. Ele envolve a função original com funcionalidade adicional (logging neste caso) sem alterar o código da função original. Este é um recurso poderoso em Python que você pode usar em muitos cenários diferentes.

Construindo um Decorador de Validação

Nesta etapa, vamos criar um decorador mais prático. Um decorador em Python é um tipo especial de função que pode modificar o comportamento de outra função. O decorador que criaremos validará os argumentos da função com base nas anotações de tipo. Anotações de tipo são uma maneira de especificar os tipos de dados esperados dos argumentos e do valor de retorno de uma função. Este é um caso de uso comum em aplicações do mundo real porque ajuda a garantir que as funções recebam os tipos de entrada corretos, o que pode evitar muitos bugs.

Entendendo as Classes de Validação

Já criamos um arquivo chamado validate.py para você, e ele contém algumas classes de validação. Classes de validação são usadas para verificar se um valor atende a certos critérios. Para ver o que está dentro deste arquivo, você precisa abri-lo no editor VSCode. Você pode fazer isso executando os seguintes comandos no terminal:

cd /home/labex/project
code validate.py

O arquivo tem três classes:

  1. Validator - Esta é uma classe base. Uma classe base fornece uma estrutura geral que outras classes podem herdar. Neste caso, ela fornece a estrutura básica para validação.
  2. Integer - Esta classe de validador é usada para garantir que um valor seja um inteiro. Se você passar um valor não inteiro para uma função que usa este validador, ele gerará um erro.
  3. PositiveInteger - Esta classe de validador garante que um valor seja um inteiro positivo. Portanto, se você passar um inteiro negativo ou zero, ele também gerará um erro.

Adicionando o Decorador de Validação

Agora, vamos adicionar uma função decoradora chamada validated ao arquivo validate.py. Este decorador executará várias tarefas importantes:

  1. Ele inspecionará as anotações de tipo de uma função. Anotações de tipo são como pequenas notas que nos dizem que tipo de dados a função espera.
  2. Ele validará os argumentos passados para a função em relação a essas anotações de tipo. Isso significa que ele verificará se os valores passados para a função são do tipo correto.
  3. Ele também validará o valor de retorno da função em relação à sua anotação. Portanto, ele garante que a função retorne o tipo de dados que deveria.
  4. Se a validação falhar, ele gerará mensagens de erro informativas. Essas mensagens dirão exatamente o que deu errado, como qual argumento tinha o tipo errado.

Adicione o seguinte código ao final do arquivo validate.py:

## Add to validate.py

import inspect
import functools

def validated(func):
    sig = inspect.signature(func)

    print(f'Validating {func.__name__} {sig}')

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ## Bind arguments to the signature
        bound = sig.bind(*args, **kwargs)
        errors = []

        ## Validate each argument
        for name, value in bound.arguments.items():
            if name in sig.parameters:
                param = sig.parameters[name]
                if param.annotation != inspect.Parameter.empty:
                    try:
                        ## Create an instance of the validator and validate the value
                        if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
                            validator = param.annotation()
                            bound.arguments[name] = validator.validate(value)
                    except Exception as e:
                        errors.append(f'    {name}: {e}')

        ## If validation errors, raise an exception
        if errors:
            raise TypeError('Bad Arguments\n' + '\n'.join(errors))

        ## Call the function
        result = func(*bound.args, **bound.kwargs)

        ## Validate the return value
        if sig.return_annotation != inspect.Signature.empty:
            try:
                if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
                    validator = sig.return_annotation()
                    result = validator.validate(result)
            except Exception as e:
                raise TypeError(f'Bad return: {e}') from None

        return result

    return wrapper

Este código usa o módulo inspect do Python. O módulo inspect nos permite obter informações sobre objetos ativos, como funções. Aqui, o usamos para examinar a assinatura da função e validar argumentos com base nas anotações de tipo. Também usamos functools.wraps. Esta é uma função auxiliar que preserva os metadados da função original, como seu nome e docstring. Metadados são como informações extras sobre a função que nos ajudam a entender o que ela faz.

Testando o Decorador de Validação

Vamos criar um arquivo para testar nosso decorador de validação. Criaremos um novo arquivo chamado test_validate.py e adicionaremos o seguinte código a ele:

## test_validate.py

from validate import Integer, PositiveInteger, validated

@validated
def add(x: Integer, y: Integer) -> Integer:
    return x + y

@validated
def pow(x: Integer, y: Integer) -> Integer:
    return x ** y

## Test with a class
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @validated
    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Agora, testaremos nosso decorador no interpretador Python. Primeiro, navegue até o diretório do projeto e inicie o interpretador Python executando estes comandos no terminal:

cd /home/labex/project
python3

Em seguida, no interpretador Python, podemos executar o seguinte código para testar nosso decorador:

>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    x: Expected <class 'int'>
    y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 83, in wrapper
    raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    nshares: Expected value > 0
>>> exit()

Como você pode ver, nosso decorador validated impôs com sucesso a verificação de tipo nos argumentos da função e nos valores de retorno. Isso é muito útil porque torna nosso código mais robusto. Em vez de deixar erros de tipo se propagarem mais profundamente no código e causar bugs difíceis de encontrar, nós os capturamos nos limites da função.

Resumo

Neste laboratório, você aprendeu sobre decoradores em Python, incluindo o que são e como operam. Você também dominou a criação de um decorador de logging simples para adicionar comportamento a funções e construiu um mais complexo para validar argumentos de função com base em anotações de tipo. Além disso, você aprendeu a usar o módulo inspect para analisar assinaturas de função e functools.wraps para preservar metadados de função.

Decoradores são um recurso poderoso do Python que permite escrever um código mais sustentável e reutilizável. Eles são comumente usados em frameworks e bibliotecas Python para preocupações transversais (cross-cutting concerns), como logging, controle de acesso e caching. Agora você pode aplicar essas técnicas em seus próprios projetos Python para um código mais limpo e sustentável.