BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Decorators do Python em profundidade

Decorators do Python em profundidade

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

Conteúdo educacional

BT