Percursos
/
Python
Python
/
Programa
/
Decoradores
De

Decoradores em Python

{one: "1 exercício", many: "%{count} exercícios", other: "%{count} exercícios"}

Sobre Decoradores

Os decoradores são funções que recebem outra função como argumento com o objetivo de estender ou substituir o comportamento da função recebida. Se a função A for um decorador e a função B for o seu argumento, então a função A modifica, estende ou substitui o comportamento da função B sem modificar o código da função B. Dizemos que a função decoradora A envolve a função B. Apesar de falarmos em "modificar" o comportamento, a função envolvida não é de facto alterada. O comportamento ou é acrescentado à volta da função envolvida (e daquilo que ela devolve), ou o comportamento da função envolvida é substituído por outro comportamento.

Um decorador é uma função de ordem superior

Uma função de ordem superior é uma função que aceita uma ou mais funções como argumentos e/ou devolve uma ou mais funções. Uma função, usada como argumento ou devolvida por outra função, é uma função de primeira classe. Uma função Python, enquanto objeto invocável, é uma função de primeira classe que pode ser guardada numa variável ou usada como argumento, tal como qualquer outro valor ou objeto. As funções de ordem superior e as funções de primeira classe trabalham em conjunto para tornar possíveis os decoradores.

Como é usar um decorador

O símbolo @ é colocado antes do nome da função decoradora, mesmo por cima da função a decorar, assim:

@decorator
def decorated_function():
    pass

Alguns decoradores aceitam argumentos:

@decorator_with_arg(name="Bob")
def decorated_function2():
    pass

Se um decorador tiver argumentos por omissão, tens de usar parênteses na chamada @decorator() para que o decorador funcione, tal como farias ao chamar qualquer função:

@decorator_with_default_arg()
def decorated_function3():
    pass

Se um decorador receber um argumento posicional, não fornecer os argumentos dá origem a um erro com um aspeto semelhante a:

TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'

A sintaxe @decorator é açúcar sintático, ou uma forma abreviada, de chamar a função decoradora e de lhe passar a função decorada como argumento. Seguem-se exemplos de formas alternativas de chamar um decorador:

def function():
    pass

function = decorator(function)


def function2():
    pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
    pass

function3 = decorator_with_default_arg()(function3)

Escrever um decorador simples

A maioria dos decoradores destina-se a estender ou substituir o comportamento de outra função, mas alguns decoradores podem não fazer nada além de devolver as funções que envolvem.

Os decoradores são funções que recebem pelo menos um argumento: a função que estão a envolver. Normalmente devolvem ou a função envolvida, ou o resultado de uma expressão que usa a função envolvida.

Um decorador simples, que apenas devolve a função que envolve, pode ser escrito assim:

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

Um decorador pode apenas acrescentar efeitos secundários, como informação adicional usada para registo:

>>> def my_logger(func):
...     print(f"Entering {func.__name__}")
...     return func
...
... @my_logger
... def my_func():
...     print("Hello")
...
>>> my_func()
Entering my_func
Hello

Um decorador não se devolve a si próprio. Pode devolver os seus argumentos de função, outra função, ou um ou mais valores que substituem o valor devolvido pela função recebida ou decorada. Se um decorador devolver outra função, normalmente devolve uma função interna.

Funções internas

Uma função pode ser definida dentro de outra função. Uma função aninhada destas chama-se função interna. Um decorador pode usar uma função interna para envolver o seu argumento de função. O decorador devolve então a sua função interna. A função interna pode depois devolver o argumento de função original.

Um decorador de validação com uma função interna

Segue-se um exemplo de um decorador usado para validação:

def my_validator(func):
    def my_wrapper(world):
        print(f"Entering {func.__name__} with {world} argument")
        
        if world == "Pluto":
            print("Pluto is not a planet!")
        else:
            return func(world)
    return my_wrapper

@my_validator
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!

Na primeira linha temos a definição do argumento func do decorador. Na linha seguinte está a definição da função interna do decorador, que envolve o argumento func. Como a função interna envolve o argumento func do decorador, recebe o mesmo argumento que é passado a func. Repara que o wrapper não tem de usar o mesmo nome para o argumento que foi definido em func. A função original usa planet e o decorador usa world, e o decorador continua a funcionar.

A função interna ou devolve func ou, se world == "Pluto", imprime que Plutão não é um planeta. Poderia, em alternativa, ser codificada para lançar um ValueError. Assim, a função interna envolve func e ou devolve func, ou faz algo que substitui aquilo que func faria. O decorador devolve a sua função interna. A função interna pode ou não devolver a função original recebida. Consoante o código que é executado condicionalmente na função envolvente ou na função interna, pode ser devolvido func, pode ser lançado um erro, ou pode ser devolvido um valor do tipo de retorno de func.

Decorar uma função que recebe um número arbitrário de argumentos

É possível escrever decoradores para funções que recebem um número arbitrário de argumentos usando a sintaxe *args e **kwargs.

Segue-se um exemplo de um decorador para uma função que recebe um número arbitrário de argumentos:

def double(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

@double
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 18

print(add(2, 3, 4, 5, 6))
#-> 40

Isto funciona para duplicar o valor devolvido pela função recebida como argumento. Se quisermos triplicar, quadruplicar, etc. o valor devolvido, podemos acrescentar um parâmetro ao próprio decorador, como mostramos na secção seguinte.

Decoradores que têm os seus próprios parâmetros

Segue-se um exemplo de um decorador que pode ser configurado para multiplicar por um valor arbitrário o valor devolvido pela função decorada:

def multi(factor=1):
     if factor == 0:
        raise ValueError("factor must not be 0")

     def outer_wrapper(func):
         def inner_wrapper(*args, **kwargs):
             return func(*args, **kwargs) * factor
         return inner_wrapper
     return outer_wrapper

@multi(factor=3)
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 27

print(add(2, 3, 4, 5, 6))
#-> 60

As primeiras linhas validam que factor não é 0. De seguida é definido o wrapper exterior. Este tem a mesma assinatura que esperamos de um decorador sem parâmetros. O wrapper exterior tem uma função interna com a mesma assinatura da função original. O wrapper interior faz o trabalho de multiplicar o valor devolvido pela função original pelo argumento passado ao decorador. O wrapper exterior devolve o wrapper interior, e o decorador devolve o wrapper exterior.

Segue-se um exemplo de um decorador parametrizado que controla se valida o argumento passado à função original:

def check_for_pluto(check=True):
    def my_validator(func):
        def my_wrapper(world):
            print(f"Entering {func.__name__} with {world} argument")
            if check and world == "Pluto":
                print("Pluto is not a planet!")
            else:
                return func(world)
        
        return my_wrapper
    return my_validator

@check_for_pluto(check=False)
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!

Isto permite alternar facilmente entre verificar Pluto ou não, e é feito sem ter de modificar my_func.

Editar via GitHub A ligação abre numa nova janela ou separador