Decoradores em Python

Decoradores são objetos que transformam, embrulham e/ou não fazem nada na declaração de outros objetos - apenas em funções e classes.

Assim como o Iterator - que eu citei no último post sobre o básico de geradores, o Decorator também é um padrão de projeto sintaticamente açucarado em Python para facilitar muita coisa e deixar o código beeem mais bonito!

Você provavelmente já deve ter visto algum decorador em alguma classe por aí. Da uma olhada no @property do seguinte código:

import math


class Person:
    def __init__(self, name, life):
        self.name = name
        self.life = life

    @property
    def dead(self):
        return math.ceil(self.life) < 1

Aquilo é um decorador. Sempre que houver nomes prefixados com @ acima de uma declaração de função ou classe, será um decorador em ação!

Quando o objeto property é decorado em algum método - ou em algum objeto -, ele é embrulhado para dentro de um descritor especial - escrito totalmente em C.

Mais exemplos

Decoradores são muito bonitos e estes simples exemplos aqui não poderiam ficar de fora!

@perfect
class Shrimp:
    pass


@very_bad
@horrible
def pudding():
    pass

Mostrando dessa forma nem da pra se imaginar o quão feio decoradores podem ficar…

Podemos decorar uma classe e função com diversos decoradores! - o máximo de decoradores que eu cheguei a ver sendo utilizado na mesma declaração foi em torno de 6 ou 7, é bem dificil ultrapassar de 3 decoradores.

E como essa maravilha funciona!!?

Ótima pergunta meu caro editor de texto! Quando escrevemos uma função decorada que nem esta aqui:

@lovely
def nium():
    pass

Estamos na verdade escrevendo de forma mais bonita ou fácil o seguinte:

def nium():
    pass


nium = lovely(nium)

Ou seja, criamos o objeto da função (linha 1 até 2), passamos pro decorador (linha 4) e o que ele retornar será salvo no nome da função (linha 4).

Bem simples, não? Não? Vamos entender melhor decorando uma função com print então!

@print
def pudding():
    pass


print(pudding)
<function pudding at 0x0000021A16AFEF70>
None

A função pudding foi criada e passada para o print, que logo foi mostrada no terminal, daí o print retornou None, que foi salvo em pudding, depois a gente imprime o valor de pudding que é None. Ou seja, em outras palavras (“códigos” soaria estranho):

def pudding():
    pass


pudding = print(pudding)
print(pudding)

Com vários decoradores podemos usar a mesma ideia, saca só:

@bool
@print
def pudding():
    pass

É a mesma coisa que

def pudding():
    pass


puddinng = bool(print(pudding))

Mão na massa adoro pizza

É possível criar um decorador para diversas coisas e, por que não um que imprime o retorno de uma função?

def imprimir(function):
    def embrulho(*args):
        result = function(*args)
        print(result)

    return embrulho


@imprimir
def somar(x, y):
    return x + y


somar(1, 1)  # 2
somar(2, 5)  # 7

É bem comum vermos closures, wrappers e funções aninhadas dentro de decoradores, provavelmente você irá ver muito um código no seguinte estilo dentro deles:

def decorator(function):
    def wrapper(*args, **kwargs):
        return function(*args, **kwargs)

    return wrapper

Basicamente o wrapper embrulha function para fazer algo a mais.


Fazer o embrulhamento de uma função dentro de um decorador e o retornar significa mudarmos a função que nos foi passada por outra e, se precisarmos acessar a documentação de function por exemplo, só conseguiremos acessar a de wrapper. Para evitar esse problema existe o decorador wraps da biblioteca functools, que é sensacional!!

from functools import wraps


def decorator(function):
    @wraps(function)
    def wrapper():
        return function()

    return wrapper


@decorator
def pudding():
    pass


print(pudding.__name__)  # pudding

Tente remover a linha 5 e ver o que acontece!

Decoradores com parâmetros…?

É possível passar parâmetros para os decoradores - na verdade, o que acontece é que nós podemos resolver uma função na sintaxe do decorator, resultando geralmente em uma closure, que acaba sendo o decorador de fato - para que possamos mudar o comportamento dele, por exemplo.

Imagine que queiramos criar um decorador que embrulhe uma função fazendo com que ela retorne o que passarmos e o que foi executado por ela mesma. Teriamos algo mais ou menos assim:

def retornar(valor):
    def decorator(function):
        def wrapper(*args, **kwargs):
            resultado = function(*args, **kwargs)
            return valor, resultado

        return wrapper
    return decorator

Basicamente criamos uma função chamada retornar que recebe valor e, nos retorna uma outra função chamada decorator que recebe uma função e a embrulha com outra função fazendo que com que ela retorne valor e o próprio resultado da função embrulhada.

@retornar(2)
def somar(x, y):
    return x + y


resultado = somar(2, 4)
print(resultado)  # (2, 6)

Essa questão de “decoradores com parâmetros” são bem difíceis de se entender na primeira vez, tente colocar alguns prints e/ou até tentar desfazer o decorador para entender melhor o fluxo de execução!

Suco de laranja

A partir do Python 3.9, a gramática dos decoradores foram relaxadas, sendo possível passar expressões ao invés de apenas nomes e argumentos - igual à chamada de uma função.

@[
    10, 25,
    30, 0, 100,
    lambda _: lambda: print(_.__name__, _(), sep='')
][-1]
def Nium():
    return "Xp"


Nium()  # Imprime NiumXp no terminal

Esse exemplo é bem tosco, mas da pra ter uma ideia do quão estranho funções e/ou classes podem aparecer com decoradores daqui pra frente.

Claro que a proposta dessa mudança não tem nada a ver com o que eu mostrei. Existem listas de decoradores em algumas bibliotecas e, não é/era fácil de se utilizar, tinham de ser feitos coisas bem estranhas…

def decorate(index, sequence, f):
    return f(sequence[index])


@decorate(5, buttons, lambda i: i.events.click)
def on_click(button):
    pass

Ficava/Fica muito ruim de se ler, além de que vários decoradores eram/são criados… Com essa mudança,podemos deixar o código da seguinte maneira:

@buttons[5].button.events.click
def on_click(button):
    pass

Bem mais bonito!

Da uma olhada na PEP-0614 para entender mais a fundo sobre essa mudança.

Classes como decoradores

Utilizar uma classe como decorador é bem simples, podemos simplesmente utilizar __init__ para receber os argumentos e o __call__ para preparar o decorador. Também é possível utilizar o __new__, mas acaba sendo mais difícil e bem específico seu uso.

Não consegui pensar em um exemplo legal para por aqui!!

Curiosidade

A PEP-0318 mostra algumas propostas de sintaxes para os decoradores e…

def @classmethod foo(arg1,arg2):
    pass


def foo @classmethod (arg1,arg2):
    pass

…já imaginou se viesse dessa forma para o Python? Seria beeem estranho.

·