quinta-feira, 29 de outubro de 2009

Python Decorators - Argumentos

Bom dia leitores!

Hoje estou dando continuidade ao post da semana passada, sobre Decorators em Python. Agradeço à todos que demonstraram interesse em saber mais sobre esse recurso da linguagem, que só agora está se tornando mais popular. Agradeço também àqueles que enviaram dúvidas por e-mail e peço que as envie nos comentários do blog, pois sua dúvida pode ser a dúvida de alguém.

Como falei na semana passada, decoradores de função não são nada mais do que funções que recebem outras funções como parâmetro e retornam um objeto (também do tipo função) que possui as características tanto da função original quanto da decoradora.

No entanto, no post passado trabalhamos somente com decoradores e funções sem argumentos. Assim eu fiz pois achei que ficaria mais fácil aprender como funcionam os decoradores se separasse o conceito de suas especialidades.


No post de hoje, trataremos de dois casos específicos de argumentos: Os argumentos da função original e argumentos da função decoradora.

Python Decorators - Argumentos



Eu não poderia falar sobre argumentos de funções e decoradores sem falar na flexibilidade com que o Python lida com o formato de argumentos. Existem três formas de se passar parâmetros em uma função em python:

1 - Lista estática de argumentos: def func(a, b, c, x=10)
2 - Lista dinâmica de argumentos: def func(*args)
3 - Lista dinâmica de argumentos-chave: def func(**kwargs)

Como acredito que todos vocês saibam, cada um desses métodos nos permite acessar a lista de parâmetros de uma maneira única. Vejamos:

def func(a, b, c)

Nesse primeiro caso, nossa função contém uma lista estática de parâmetros, resolvidos na ordem em que aparecem. Por ser uma lista estática, devemos sempre usar todos os parâmetros para que nossa função possa ser validada corretamente.

def func(*args)

Agora a coisa começa a ficar interessante. Nesse exemplo, nós temos uma lista dinâmica de argumentos, o que significa que não estamos limitados a usar uma quantidade definida de argumentos. Podemos usar a quantidade que acharmos interessante. Os argumentos informados são armazenados sob a forma de um objeto lista, nomeado nesse caso como args. Exemplo:

>>> def somatudo(*args):
...   retorno = 0
...   for x in args:
...     retorno += x
...   return retorno
... 
>>> somatudo(10, 20, 30)
60

def func(**kwargs)

Uma outra forma de informar dinamicamente argumentos para nossa função é através de argumentos-chave. A diferença desde para o anterior se dá pelo uso de um dicionário no lugar de uma lista. Exemplo:

>>> def listak(**kwargs):
...   for x in kargs:
...     print '{0}: {1}'.format(x, kwargs[x])
... 
>>> listak(x=10, y=50)
y: 50
x: 10

É perfeitamente possível mesclarmos os três tipos de chamadas, mesclando argumentos estáticos com dinâmicos. No entanto, não pretendo me estender neste assunto, que com certeza renderia um ótimo post sobre funções. Se quiser saber mais, veja no artigo Python 101 - Introducion to Python

Bom, qual o objetivo disso afinal? Já irei explicar ...

Tratando os argumentos da função decorada

Se você se lembram bem, nosso decorador da função anterior funciona perfeitamente para uma função sem parâmetros, certo? E se precisássemos passar o mesmo decorador para uma função que recebe parâmetros?

>>> 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(a, b):
...   print "Função 1"
...   return a+b

Na hora da criação, não teríamos nenhum problema. Na verdade, o decorator decora a função sem levantar nenhum aviso. O problema ocorre quando vamos chamar nossa função com argumentos:

>>> func2(2, 3)
Traceback (most recent call last):
File "", line 1, in 
TypeError: __call__() takes exactly 1 argument (3 given)

Se você notar, o erro ocorre lá na chamada do método __call__() da classe decoradora entrada_saida. Isso ocorre porque a __call__ não possui essa quantidade de argumentos. Ela só espera o atributo self, explícito em sua chamada. Isso é fácilmente resolvido se nós usarmos argumentos dinâmicos.

Relembrando o pequeno tutorial de argumentos, anteriormente, para que nosso decorador funcione com os argumentos da função que ele está decorando, vamos usar lista dinâmica de argumentos. Nossa classe ficará então, assim:

>>> class entrada_saida(object):
...   def __init__(self, f):
...     self.f = f
...   def __call__(self, *args):
...     print "Entrando em {0}".format(self.f.__name__)
...     self.f(*args)
...     print "Saindo de {0}".format(self.f.__name__)

Agora nossa função recebe corretamente os argumentos que forem informados. Veja que tudo o que estamos fazendo é manipular o modo como a função decorada funciona. Se precisarmos pegar o retorno da função, devemos tratar isso também. Argumentos chaves, se necessários, também devem ser tratados na função.

Uma implementação que levasse em conta estes casos poderia ser feita da seguinte forma:

>>> class entrada_saida(object):
...   def __init__(self, f):
...     self.f = f
...   def __call__(self, *args, **kwargs):
...     print "Entrando em {0}".format(self.f.__name__)
...     retorno = self.f(*args, **kwargs)
...     print "Saindo de {0}".format(self.f.__name__)
...     return retorno

Se você testar esta classe agora, verá que ela funciona para qualquer tipo de função que você queira decorar. Usando uma função para decorar, nosso código ficaria assim:

>>> def entrada_saida(f):
...   def novo_f(*args, **kwargs):
...     print "Entrando em {0}".format(f.__name__)
...     retorno = f(*args, **kwargs)
...     print "Saindo de {0}".format(f.__name__)
...     return retorno
...   return novo_f

Decoradores com parâmetros

Nós vimos até agora como fazer para passar parâmetros para a função decorada. Agora, vamos ver algo para complementar e finalizar nosso estudo sobre decoradores: passagem de parâmetros para a função decoradora.

Como vimos em todos os exemplos, um decorador recebe como parâmetro uma função a ser decorada. No entanto, isso não nos impede que possamos passar outros parâmetros para a função decoradora. Vamos ano nosso exemplo:

>>> class entrada_saida(object):
...   def __init__(self, f, a):
...     self.a = a
...     print "Argumento a: {0}".format(self.a)
...     self.f = f
...   def __call__(self, *args, **kwargs):
...     print "Argumento a: {0}".format(self.a)
...     print "Entrando em {0}".format(self.f.__name__)
...     retorno = self.f(*args, **kwargs)
...     print "Saindo de {0}".format(self.f.__name__)
...     return retorno
... 
>>> @entrada_saida(a=1)
... def x(a, b): return a+b
... 
Traceback (most recent call last):
File "", line 1, in 
TypeError: __init__() takes exactly 3 non-keyword arguments (1 given)
>>> @entrada_saida(1)
... def x(a, b): return a+b
... 
Traceback (most recent call last):
File "", line 1, in 
TypeError: __init__() takes exactly 3 arguments (2 given)

Infelismente, como você pode ver, a tática de declarar uma variável na lista de argumentos de __init__() não funciona. Isso porque, quando é necessário passar argumentos para a função decoradora, seu funcionamento se altera.

A questão é que, quando a função decoradora precisa receber argumentos, a função a ser decorada não é passada entre a lista de parâmetros. Pos isso, esta abordagem não funciona. O que acontece é que neste caso, havendo argumentos a serem passados para o decorador, o método __call__() é que é chamado para fazer o papel de decorador da função.

>>> class decorador_params(object):
...   def __init__(self, arg1):
...     print "Dentro de __init__()"
...     self.arg1 = arg1
...   def __call__(self, f):
...     print "Dentro de __call__()"
...     def funcao_decoradora(*args, **kwargs):
...       print "Dentro da função funcao_decoradora()"
...       print "Argumento da declaração do Decorador: {0}".format(self.arg1)
...       retorno = f(*args, **kwargs)
...       print "fim de f(*args, **kwargs)"
...       return retorno
...     return funcao_decoradora
... 
>>> @decorador_params("teste")
... def soma(a, b): return a+b
... 
Dentro de __init__()
Dentro de __call__()
>>> soma(2, 3)
Dentro da função funcao_decoradora()
Argumento da declaração do Decorador: teste
fim de f(*args, **kwargs)
5
>>> soma(2, 5)
Dentro da função funcao_decoradora()
Argumento da declaração do Decorador: teste
fim de f(*args, **kwargs)
7

Se você olhar atentamente, na hora da criação do decorador, verá que primeiro o decorador é criado, com os parâmetros e depois, na sua invocação (chamada ao método __call__) a função é passada. Isso é equivalente, no modo antigo, a fazer:

temp = decorate(message)
f = temp(f)

Como você pode ver, apesar de o decorador em si ser diferente só na questão de ter um parâmetro, no fundo as diferenças no funcionamento do decorador são grandes.

Isso pode ser um pouco confuso quando você não está acostumado a pensar dessa forma, principalmente com essa abordagem usando classes. Neste caso, uma abordagem baseada em uma função decoradora é melhor, e você pode então vislumbrar a divisão de cada uma das etapas:

>>> def decorador_func_params(arg1):
...   print "Dentro de decorador_func_params"
...   def func(f):
...     print "Dentro de func"
...     def func_params(*args, **kwargs):
...       print "Dentro de func_params. Argumento: {0}".format(arg1)
...       retorno = f(*args, **kwargs)
...       return retorno
...     return func_params
...   return func
... 
>>> @decorador_func_params("teste")
... def soma(a, b): return a+b
... 
Dentro de decorador_func_params
Dentro de func
>>> soma(2, 3)
Dentro de func_params. Argumento: teste
5

Uma coisa interessante: Não sei se vocês notaram, mas a função decorador_func_params() retorna func(), que por sua vez retorna a função func_params(). Isso acontece porque, durante o processo de decoração, a primeira função guarda os argumentos do decorador, a segunda a função a ser decorada e por último os argumentos a serem passados para a função. Por isso os três níveis. Uma outra maneira de se fazer isso, usando lambda, pode ser vista à seguir:

decorador_lambda = lambda decorador: lambda *args, **kwargs: lambda func: decorador(func, *args, **kwargs)

Bom, é isso. Acho que já dei muito assunto pra vocês estudarem, não foi? Agora vocês entenderam o porque de eu ter separado o tratamento de argumentos do primeiro post? Não que seja difícil entender isto, mas são duas estruturas com funcionamento diferente, sendo usadas para fazer o "açúcar sintático" que muitos dizem ser o recurso.

Espero que tenham gostado dos posts. Até a próxima postagem.

Fontes:

PEP 318
Python Decorators Don't Have to be (that) Scary
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!