sábado, 28 de dezembro de 2013

Decoradores no Python (Python Decorators)

Hora de mostrar um pouco da mágica do Python.
Vamos falar de decoradores do Python (Python decorators).
Mas antes algumas definições.

O que é um Decorador


Decoradores também são conhecidos como Wrappers (embalagens, invólucros). E isto já dá uma boa idéia de como eles funcionam. O objetivo de um decorador é "embalar" ou envolver um objeto adicionando alguma funcionalidade ou recurso. Em termos práticos, um decorador traz o objeto que ele decora (encolve) para dentro dele e desta forma pode executar outras operações antes e/ou depois do objeto em si. Assim o decorador consegue adicionar funcionalidades sem a necessidade de modificar o objeto.

O que NÃO é um Decorador


O nome "decorador" naturalmente remete ao padrão de projeto Decorator Pattern. Mas, ainda que seja possível seguir este padrão utilizando os decoradores do Python, este recurso é muito mais amplo e poderoso. Não é correto dizer que os decoradores do Python são uma implementação do Decorator Pattern.
Além do nome, outro fator que pode confundir os desavisados é a sua sintaxe especial. Apesar das semelhanças, os decoradores do Python NÃO são, nem de longe, semelhantes às Annotations do Java.  Estas são um recurso para associar metadados a objetos,  bastante explorado por frameworks naquela linguagem. São coisas completamente diferentes. Além do mais, para todos os efeitos, Python tem sua própria implementação de annotations, embora essa seja outra discussão.

Decoradores no Python: um exemplo


Decoradores são comumente reconhecidos no Python pela sua sintaxe especial:


No Python, um decorador é uma função que recebe outra função como argumento e retorna uma função. Complicado? Vamos ver na prática.
Imagine que você tem uma série de funções que realizam cálculos tributários complexos. Os valores que servem de base para estes cálculos mudam com frequência e são arbitrariamente fornecidos por outras fontes.
Uma destas funções poderia ser como esta:

Muita coisa pode dar errado numa situação destas. Alguns valores nem sempre serão fornecidos ou não estarão disponíveis naquele momento. Porém, a implementação da função não deve sofrer alterações em virtude de sua complexidade. Agora, imagine que você quer, pelo menos, prevenir que a função seja executada com valores nulos e jogue um erro bem na cara do usuário.
Para uma função apenas, isso não é um desafio. Mas vamos fazer algo que possa ser reaproveitado em outras funções do nosso sistema contábil.
Então, vamos ver o que os decoradores podem fazer por nós neste caso.
Não se preocupe com o código acima por enquanto. Por ora, basta saber que se trata de um decorador que, quando aplicado a uma função, vai substituir, em tempo de execução, cada argumento nulo da função por um valor padrão definido no momento da definição do decorador.
Vamos aplicar o decorador na nossa função:

O resultado disso é que qualquer parâmetro None será substituído pelo valor padrão definido para esta função, neste caso, zero.

A grande vantagem é que podemos reaproveitar este decorador em qualquer outra função.
Este exemplo é apenas uma amostra do que é possível fazer com decoradores. Hora de entender como isso tudo funciona.

Como funcionam os decoradores


Como já foi dito, decoradores são apenas funções que se aproveitam de uma característica especial da linguagem Python.
No Python, as funções são objetos e isso as torna muito mais poderosas e flexíveis que na maioria das linguagens de programação. Vamos brincar um pouco com essa flexibilidade (código em Python 3):
O fato de as funções serem objetos no Python nos permite reatribuir uma função a uma nova variável, associar membros a funções, passar funções como parâmetro para outras funções e muito mais.
E é justamente a parte final do exemplo acima que nos mostra a característica das funções que possibilita o funcionamento dos decoradores. Se funções podem ser passadas via parâmetro então podemos estender recursos de funções dentro de outras funções.
A essa altura, aquele exemplo de decorador que vimos anteriormente já deve fazer mais sentido. O decorador limpa_nulos nada mais é do que uma função que recebe outra função por parâmetro e estende a sua funcionalidade.
Mas note que limpa_nulos define outras duas funções internamente. Por que isso?

Algumas experiências


O terminal é seu amigo. Vamos ver o que podemos fazer com essa característica especial das funções no Python que nos ajude a entender o decorador limpa_nulos.
Começaremos com o mais básico:
Acima temos uma função que apenas retorna a mesma função que recebe. Nada complicado. Agora, se quisermos fazer algo de útil antes de chamar a função que recebemos, por exemplo, informar qual função está sendo chamada, vamos precisar definir uma função interna. Por que?
Observe o que acontece se tentarmos do jeito mais simples:
Como podemos ver, o problema é que qualquer coisa definida dentro da nossa função info será executada somente quando esta função for chamada, e não quando a função que ela retorna (func) é chamada.
Então vamos fazer uma modificação:
Note que agora não estamos retornando a função original. Na verdade, estamos retornando a função que definimos internamente e esta, por sua vez, retorna a função original somente quando é chamada.
Assim conseguimos executar o que quisermos toda vez que a função decorada for chamada.
É importante observar que, diferente do que definimos na função limpa_nulos, nossa função interna neste caso não está declarando argumentos genéricos (*args, **kwargs) . Você só precisará declarar estes argumentos caso precise ler ou alterar a lista/dicionário de argumentos da função decorada, como acontece na limpa_nulos. Em qualquer outra situação, é bem mais simples não declarar argumento algum.
Outra razão para definir uma função interna é declarar um argumento para o decorador.

Flexibilizando os decoradores


Nos exemplos anteriores o nosso decorador recebia uma função como argumento. Outra possibilidade é definirmos argumentos para o próprio decorador, como o valor_padrao da função limpa_nulos. Isso permite construções ainda mais avançadas para os decoradores. Por exemplo, podemos criar um decorador que vai executar novamente caso a função que recebe registre uma exceção. E neste caso, um parâmetro pode definir quantas vezes queremos que a função seja executada novamente.
Agora vamos adaptar o exemplo anterior para implementar um decorador com argumento:
Continuamos tratando os decorados como simples funções (o que de fato são), mas, sobretudo no exemplo de decoradores com argumentos, fica bem evidente que a sintaxe especial dos decoradores é bem mais intuitiva. Veja a diferença, no mesmo exemplo acima:
Agora deve ser bem mais fácil compreender o exemplo inicial do artigo e todo o poder dos decoradores no Python.

Sobre funções, métodos e decoradores


Métodos são apenas um tipo especial de função no Python. Neste artigo utilizei o termo "função" de uma forma mais ampla para facilitar o entendimento. De uma forma geral, qualquer decorador que funciona em uma função deve funcionar igualmente bem em um método com algumas exceções.
As exceções, no caso, são para as ocasiões em que o decorador necessita acessar o objeto recebido pelo método dinamicamente.
Lembre-se de que no Python o primeiro argumento de um método (tradicionalmente o self) é o objeto que o acionou. Uma confusão comum é tentar acessar o self dentro do decorador, ou até tentar passar o self como parâmetro.
Lembre-se de que o decorador e o método são chamados em momentos e escopos diferentes da execução da aplicação.
Se o decorador precisa acessar algum atributo da instância que acionou o método decorado, basta declarar a lista de argumentos genéricos na função interna do decorador e acessar o primeiro argumento da lista, que, como já vimos, é o objeto passado para o método.
Porém, uma construção mais adequada para este tipo de tarefa pode ser criada com a ajuda dos descritores do Python. Mas isso é assunto para um artigo futuro.



Atualização:
Ótima dica do Vinicius Assef, estou adicionando um link para um excelente artigo que pode ajudar aqueles que ainda não conseguiram "sacar" os decoradores do Python:
Python Decorators
Além disso, vou adicionar também outros dois artigos que me ajudaram neste assunto:
Improve Your Python: Decorators Explained
Painless Decorators