BT

Decorators do Python em profundidade

| por Felipe Volpone Seguir 0 Seguidores em 29 abr 2015. Tempo estimado de leitura: 5 minutos |

Decorators (decoradores) do Python são extensivamente utilizados pela comunidade, contudo em diversas vezes, seu uso acaba sendo superficial pois não explora toda a potencialidade desta técnica. Este artigo apresenta em profundidade esta poderosa técnica, através de uma série de exemplos de forma a deixar seu código Python mais limpo e legível.

Para se aprofundar nesta técnica, vamos analisar alguns exemplos básicos:

def decorator_simples(funcao_chamada):
        return funcao_chamada(*args, **kwargs)
@decorator_simples
def diz_ola():
        print 'ola'

Antes de entrar em detalhes de como implementar decorators, é preciso entender que decorators são apenas um syntactic sugar (açúcar sintático) da linguagem. Na verdade, o interpretador do Python transforma o código do primeiro exemplo no seguinte código:

diz_ola = decorator_simples(diz_ola)
$ diz_ola()
>>> 'ola'

Ou seja, no primeiro exemplo, a função decorator_simples recebe como parâmetro a função que foi decorada e retorna a própria função passada; assim o retorno é recebido pela variável diz_ola que depois é chamada. Vamos a mais um exemplo:

def imprime(funcao):
    def inner():
        print(funcao)
    return inner
@imprime
def diz_ola():
        return 'ola'
$ diz_ola()
>>> <function diz_ola at 0x10c906c08>

Aqui está um ponto importante da linguagem, pois estamos trabalhando com clojures, ou seja, funções dentro de funções. A função inner é declarada dentro de imprime e é retornada pela mesma. Dessa maneira a função diz_ola é sobrescrita pela função que retornou do decorator. Mas este não é um comportamento desejado para essas funções, a ideia é adicionar comportamento, não excluí-lo. O exemplo abaixo é mais completo:

def logger(funcao_chamada):
        def inner(*args, **kwargs):
                print funcao_chamada
                return funcao_chamada(*args, **kwargs)
        return inner
@logger
def diz(msg='ola'):
        print 'ola'
$ diz()
>>> <function diz at 0x10c906c08>
>>> 'ola'

Observe que neste exemplo, foi mantido o comportamento natural da função "diz" e ainda adicionado um comportamento extra, que imprime a função decorada. Outro detalhe importante: *args e **kwargs são os argumentos passados pela função. Os parâmetros *args são passados seqüencialmente, e **kwargs são os parâmetros nomeados (*args é uma lista e **kwargs é um dicionário).

Agora que foi obtida uma maior compreensão do funcionamento dos decorators, é possível começar a pensar em como combinar o seu uso. Veja o exemplo:

def timestamp(funcao_chamada):
        def inner(*args, **kwargs):
                from datetime import date time
                print 'funcao chamada em %s' % (datetime.now())
                return funcao_chamada(*args, **kwargs)
        return inner
def logger(funcao_chamada):
        def inner(*args, **kwargs):
                print funcao_chamada
                return funcao_chamada(*args, **kwargs)
        return inner
@timestamp
@logger
def diz_ola():
        print 'ola'
$ ola()
>>> chamado em 2015-04-12 14:31:50.548640

>>> <function diz_ola at 0x1107506e0>
>>> ola

O código acima é interpretado pelo interpretador do Python da seguinte forma:

diz_ola = timestamp(logger(diz_ola))
$ diz_ola()
>>> chamado em 2015-04-12 14:31:50.548640
>>> <function diz_ola at 0x1107506e0>
>>> ola

Ou seja, o decorador logger, receberá a função retornada pelo decorator timestamp. Mas e se o objetivo fosse que o decorator suportasse parâmetros? Veja como este objetivo pode ser atingido:

def usuario_valido(usuario, msg):
    def decorator(funcao_chamada):
        def inner(*args, **kwargs):
            if usuario:
                return funcao_chamada(*args, **kwargs)
            else:
                raise AttributeError, msg
        return inner
    return decorator
@usuario_valido(True, "usuario eh valido")
def diz_ola():
    print 'ola'
$ diz_ola()
>>> usuario eh valido
@usuario_valido(False, "usuario nao eh valido")
def diz_ola():
        print 'ola'
$ diz_ola()
>>> AttributeError: usuario nao eh valido

Embora este exemplo possa parecer complexo, ele na verdade é apenas uma evolução dos exemplos anteriores. A função usuario_valido, recebe parâmetros (passados no próprio decorator) e retorna o decorator que ela declarou (função decorator), o restante do código é análogo aos exemplos anteriores

Vale lembrar que neste caso, a chamada do interpretador Python será realizada da seguinte forma:

diz_ola = usuario_valido(True, "usuário não eh valido")(diz_ola)

Neste ponto, fica claro que além de flexíveis, os decorators do Python são muito poderosos e é possível encontrá-los sendo usados em diversas situações diferentes, tais como: mapeamento de urls, configuração de content-type de serviços, testes unitários e em diversas outros casos. Veja os exemplos de forma mais detalhada neste gist.

Porém, todos os exemplos apresentados estão de certa forma incompletos. Observe o motivo no exemplo:

def logger(funcao):
    def inner(*a, **k):
        return funcao(*a, **k)
    return inner
@logger
def diz_ola():
        """
                doc desta funcao. eh retornado no método  __doc__
"""
print 'ola'
$ diz_ola.__name__
>>> 'inner'
$ diz_ola.__doc__
>>> ''

Ao usarmos os decorators dessa maneira, perdemos valores importantes dos antigos métodos, que agora foram sobrescritos pelos novos. Poderíamos fazer um workaround definindo dentro da função da logger os atributos __name__ e __doc__ da função original, por exemplo:

def logger(funcao):
    def inner(*a, **k):
        return funcao(*a, **k)
    inner.__name__, inner.__doc__ = funcao.__name__, funcao.__doc__
    return inner

Porém, além deste código ficar de uma maneira não tão pythonica e legível, se usarmos o módulo inspect, veremos que os argumentos também se perderam. Veja:

Sem decorator:

def diz(msg):
    print msg
$ import inspect
$ inspect.getargspec(diz)
>>> ArgSpec(args=['msg'], varargs=None, keywords=None, defaults=None)

Com decorator:

@logger
def diz(msg):
	print msg 
$ import inspect
$ inspect.getargspec(msg)
>>> ArgSpec(args=[], varargs=‘a’, keywords=‘k’, defaults=None)

Não é o objetivo deste artigo entrar em tantos detalhes, mas uma abordagem para solução destes problemas é através do módulo wrapt (maiores detalhes neste link).

Graham Dumpleton, um grande pythonista da comunidade, criou este módulo que nos ajuda a construir decorators. O código abaixo, mostrar como criar decorators usando a biblioteca wrapt.

import wrapt
@wrapt.decorator
def logger(funcao_chamada, instance, args, kwargs):
    print funcao_chamada
    return funcao_chamada(*args, **kwargs)
@logger
def diz(msg='ola'):
    print msg
$ diz()
>>> <function diz at 0x102563668>
>>> 'ola'
$ print diz.__name__
>>> 'diz'
$ import inspect
$ inspect.getargspec(diz)
>>> ArgSpec(args=['msg'], varargs=None, keywords=None, defaults=('ola',))

Decorators do Python é um assunto bem amplo e extensivamente utilizado pela comunidade, contudo, em diversas vezes seu uso acaba sendo superficial, pois não explora toda a potencialidade desta técnica. Para 95% do casos, os primeiros exemplos de decorators, sem o uso de wrapt, irão funcionar e resolver o problema, contudo, para os 5% restantes, é necessário ter maior atenção e buscar um entendimento mais aprofundado do assunto.


Sobre o autor

profile-photo-FelipeVolpone-96x96.jpg

Escritor caseiro, desenvolvedor em eterna formação e pythonista. Acredita que assim como as palavras, o software tem poder de se expressar, transmitir e transformar ideias. Atualmente trabalha como desenvolvedor na Dextra.

Avalie esse artigo

Relevância
Estilo/Redação

Olá visitante

Você precisa cadastrar-se no InfoQ Brasil ou para enviar comentários. Há muitas vantagens em se cadastrar.

Obtenha o máximo da experiência do InfoQ Brasil.

Dê sua opinião

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão
Comentários da comunidade

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

HTML é permitido: a,b,br,blockquote,i,li,pre,u,ul,p

Receber mensagens dessa discussão

Dê sua opinião

Faça seu login para melhorar sua experiência com o InfoQ e ter acesso a funcionalidades exclusivas


Esqueci minha senha

Follow

Siga seus tópicos e editores favoritos

Acompanhe e seja notificados sobre as mais importantes novidades do mundo do desenvolvimento de software.

Like

Mais interação, mais personalização

Crie seu próprio feed de novidades escolhendo os tópicos e pessoas que você gostaria de acompanhar.

Notifications

Fique por dentro das novidades!

Configure as notificações e acompanhe as novidades relacionada a tópicos, conteúdos e pessoas de seu interesse

BT