sábado, 24 de outubro de 2009

Python Decorators - Uma Introdução

Durante meu aprendizado em Python, um dos grandes pontos que sempre tive dificuldade de entender era o uso dos tais decoradores de função. Muitos tentaram me explicar, li vários textos mas só entendi realmente quando vi uma aplicação que efetivamente usava os tais decoradores para resolver alguns problemas.

Em vista dessa dificuldade natural de se entender como funcionam essas estruturas dentro do Python, me dispus a escrever este post explicando de uma forma fácil como funciona este recurso de linguagem.

Decorators - Uma introdução



Antes de mais nada, os decoradores em Python são bem diferentes do padrão de projeto chamado Decorator. Você pode, no entanto, usar decoradores para implementar o padrão de projeto. O recurso de decoradores do Python pode ser melhor entendido quando comparado com uma macro, como o pré-processador de texto do C.


O que são Macros?

De uma forma geral, uma macro é uma forma de se poder alterar elementos da linguagem. Um exemplo em C:

#define max(A,B) ( (A) > (B) ? (A):(B))

Se utilizarmos isso no código:

x = max(q+r,s+t);

O pré-processador transformará na seguinte expressão:

x = ( (q+r) > (r+s) ? (q+r) : (s+t));

Como você pode ver, ele simplesmente substitui o nome da macro pela sua definição, o que pode ser usado como uma ferramenta para diminuir a quantidade de código redundante, mas ainda não é bem o que o Python faz. O Python faz mais além disso.

Então, o que os decoradores fazem em Python? Eles modificam as funções e, no caso de decoradores de classes, modificam uma classe inteira.

O que fazer com Decorators?

Como citado antes, decoradores modificam funções, alterando ou injetando código nas mesmas. Isso pode soar bastante parecido com programação orientada à aspectos em Java. No entanto, ao contrário do Java, a implementação de decoradores é muito mais simples e mais poderosa que a citada. Por exemplo, suponhamos que você quer fazer algo quando o escopo de execução entra e sai de uma função (por exemplo, verificar segurança, fazer log de chamadas, etc). Com decoradores, isso ficaria da seguinte forma:

@entrada_saida
def func1():
    print "Dentro da função 1."

@entrada_saida
def func2():
    print "Dentro da função 2."

O @ no nosso caso, indica que estamos tratando de um decorador a ser aplicado nas funções func1() e func2().

Decoradores de Funções

Um decorador de função é aplicado à uma definição de função colocando-se o decorador na linha anterior ao da definição da função. Exemplo:

@meu_decorador
def minha_funcao():
    print "Dentro da função minha_funcao()"

Quando o compilador passa sobre essa parte do código, minha_funcao() é compilada e o objeto função resultante é passado para o @meu_decorador, que fará algo para produzir um objeto do tipo função que então substituirá a função original minha_funcao().

Com o que esse @meu_decorador pode se parecer? Bom, os vários exemplos disponíveis por aí na internet costumam usar uma função como decorador, mas essa não é uma forma interessante de se olhar para os decoradores, principalmente quando não temos a idéia de que tudo em Python são objetos, inclusive funções.

Em nosso caso, para facilitar o entendimento, vamos criar uma classe para criar nosso decorador. Os motivos para fazer isso é que podemos construir algo mais poderoso usando uma classe que uma função. Outro para usarmos uma classe é que podemos simular seu uso como uma função, implementando o método __call__(), método que é chamado no caso de invocarmos nossa classe como um método. Então, qualquer classe que venhamos a usar como decoradores, devem implementar essa função.

Bem, o que nosso decorador fará? Bem, ele pode fazer qualquer coisa, no entanto esperamos que em algum ponto da sua execução, o código da função original deverá ser usado. No entanto, isso não é uma exigência. Vamos ao exemplo:

class meu_decorador(object):

    def __init__(self, f):
        print "Chamada de dentro da função meu_decorador.__init__()"
        f()

    def __call__(self):
        print "Chamada de dentro da função meu_decorador.__call__()"

@meu_decorador
def minha_funcao():
    print "Chamada de dentro da função minha_funcao()"

minha_funcao()

Quando você rodar este código, você verá a seguinte saída:

>>> class meu_decorador(object):
...   def __init__(self, f):
...     print "Chamada de dentro da função meu_decorador.__init__()"
...     f()
...   def __call__(self):
...     print "Chamada de dentro da função meu_decorador.__call__()"
... 
>>> @meu_decorador
... def minha_funcao():
...   print "Chamada de dentro da função minha_funcao()"
... 
Chamada de dentro da função meu_decorador.__init__()
Chamada de dentro da função minha_funcao()
>>> minha_funcao()
Chamada de dentro da função meu_decorador.__call__()
>>> 

Note que o construtor para o decorador é executado no momento em que a função será decorada. Tanto é verdade que podemos chamar a função de dentro do método __init__(), demonstrando que a função encontra-se criada no momento que ela vai ser decorada com a classe.

Normalmente, você captura a função no construtor para utilizarmos posteriormente dentro do método __call__(). De fato, a decoração e a execução do método __call__() são duas fases distintas do decorador. Por isso é muito mais fácil enxergar isso usando classes, e permite criar estruturas mais poderosas.

Continuando, quando a função minha_funcao() é chamada após ter sido decorada, nós temos então um comportamento completamente diferente: o método meu_decorador.__call__() é chamado no lugar do código original. Isso acontece porque o ato de se decorar uma função substitui a função original com o resultado da decoração: em nosso caso, meu_decorador() substitui minha_funcao().

Isso pode não parecer muito claro, mas anteriormente à introdução dos decoradores, para se conseguir algo análogo a isto, precisávamos, por exemplo, passar nossas funções como parâmetros para a função que irá servir de decorador, como no exemplo a seguir:

def bogus(): pass
bogus = staticmethod(bogus)

Com a adição do operador @, ficou um pouco mais fácil:


@staticmethod
def bogus(): pass


Esse é o grande motivo pelo qual algumas pessoas reclamam dos decoradores. Porque, no final, não é nada mais do que um açúcar sintático, que significa: "passe o objeto função para outra função e substitua o resultado no lugar da função original".

O poder atrás dos decoradores

Voltemos ao nosso primeiro exemplo. Inicialmente tinhamos comentado que queríamos saber o momento exato em que a execução entra e sai do escopo de nossa função, não é mesmo? Com a explicação, fica um pouco mais fácil perceber o que fazer, não é mesmo? Vamos ao código resultante, então:

class entrada_saida(object):

    def __init__(self, f):
        self.f = f

    def __call__(self):
        print "Entrando em {0}".format(self.f.__name__)
        self.f()
        print "Saindo de {0}".format(self.f.__name__)

@entrada_saida
def func1():
    print "Função 1"

@entrada_saida
def func2():
    print "Função 2"

func1()
func2()


A saída resultante, você vê aki:

>>> class entrada_saida(object):
...   def __init__(self, f):
...     self.f = f
...   def __call__(self):
...     print "Entrando em {0}".format(self.f.__name__)
...     self.f()
...     print "Saindo de {0}".format(self.f.__name__)
... 
>>> @entrada_saida
... def func1():
...   print "Função 1"
... 
>>> @entrada_saida
... def func2():
...   print "Função 2"
... 
>>> func1()
Entrando em func1
Função 1
Saindo de func1
>>> func2()
Entrando em func2
Função 2
Saindo de func2
>>> 

Você pode ver que as funções decoradas agora contém marcações de entrada e saída em torno da chamada da função. No nosso exemplo, usamos a propriedade __name__ para mostrar o nome da função armazenada em self.f que veio através do argumento do método __init__, e em seguida, executamos a função.

Usando funções como decoradores

A única restrição de um decorador é que o mesmo deve ser executável, ou em outras palavras, implementar o método __call__(). Isso nos permite, por exemplo, usar uma função como um decorador, que é o método mais visto em uso hoje, em eventuais códigos que você venha a encontrar na internet.

Voltando ao exemplo anterior, poderíamos implementar o mesmo decorador utilizando uma função, da seguinte maneira:

def entrada_saida(f):
def novo_f():
    print "Entrando em {0}".format(f.__name__)
    f()
    print "Saindo de {0}".format(f.__name__)
    return novo_f

@entrada_saida
def func1():
    print "Função 1"

@entrada_saida
def func2():
    print "Função 2"

func1()
func2()

novo_f() é definido dentro do corpo da função entrada_saida(), então ele é criado e então é retornado quando o decorador entrada_saida é chamado. Note que novo_f() é uma função única, porque ela utiliza o valor de f informado na hora que novo_f() foi criada.

Um detalhe interessante. Se você chamar func1.__name__, perceberá que o nome da função não é o da que você definiu e sim o da função interna do decorador. Se isso for algum problema, você pode redefinir o nome, alterando a função entrada_saida() para a seguinte forma:

def entrada_saida(f):
        def novo_f():
            print "Entrando em {0}".format(f.__name__)
            f()
            print "Saindo de {0}".format(f.__name__)
            novo_f.__name__ = f.__name__
            return novo_f

Está resolvido o problema do nome. No fundo a função é decorada, mas fora da definição, ninguém mais precisa saber que a função em questão é só um decorador para a função realmente definida abaixo dele.

Agora que você sabe o básico sobre decoradores, pode ler mais sobre decoradores no wiki do site oficial do Python. Note que em vários exemplos, são utilizadas classes no lugar de funções, por sua robustez de definição, melhor que uma função em vários casos.

Intencionalmente, não lidei com passagem de parâmetros nesse post pois tornaria muito extensa a leitura, e misturaria muitos conceitos em um só lugar. Então, tomei a decisão de falar sobre parâmetros de funções em um próximo (e quem sabe breve) post sobre esse assunto.

Espero ter alcançado com esse post o intento original de se explicar um pouco mais sobre o funcionamento básico dos decoradores em Python. No começo parece meio difícil entender, mas depois a coisa fica bem mais simples.

Fontes:

PEP 318
Charming Python: Decorators make magic Easy
@decorators
Python Decorator Library
Python Decorators Syntatic Sugar

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!