domingo, 3 de dezembro de 2017

Decorators em Python - parte 1

Decorators em Python - Parte 1


Python @decorator
Python é rico em recursos poderosos. Um dos mais interessantes são os decoradores, decorators. No contexto dos padrões de design, decoradores alteram dinamicamente a funcionalidade de uma função, método ou classe sem ter que usar diretamente subclasses. Isso é ideal quando você precisa ampliar a funcionalidade de uma função que não deseja modificar. Podemos implementar o padrão decorator em qualquer lugar, mas Python facilita sua implementação fornecendo uma sintaxe expressiva para isso: @.
Essencialmente, os decoradores funcionam como wrappers, modificando o comportamento do código antes e depois da execução de uma função alvo, sem a necessidade de modificar a própria função, aumentando a funcionalidade original, decorando-a.
Mas antes de mergulhar no assunto de @decorators, alguns pontos devem ser explicados. Em Python, funções são cidadãos de primeira classe, são objetos e isso significa que podemos fazer muitas coisas úteis com elas.

Vejamos, por exemplo, as funções abaixo e como elas se comportam.

Uma função pode ser atribuída a uma variável

def cumprimentar(nome):
    return "Olá " + nome

cumprimentar_alguem = cumprimentar
print(cumprimentar_alguem("Francisco"))
Olá Francisco

Uma função pode ser criada dentro de outra

def cumprimentar(nome):
    def get_mensagem():
        return "Olá "
    result = get_mensagem() + nome
    return result

print(cumprimentar("Francisco"))
Olá Francisco

Uma função pode ser passada como parâmetros para outra função

def cumprimentar(nome):
    return "Olá " + nome

def testar_funcao(func):
    nome = 'Francisco'
    return func(nome)

print(testar_funcao(cumprimentar))
Olá Francisco

Uma função pode retornar outra função

Em outras palavras, funções podem gerar outras funções.

def compor_cumprimento():
    def get_mensagem():
        return "Olá! Tudo bem?"
    return get_mensagem

cumprimentar = compor_cumprimento()
print(cumprimentar())
Olá! Tudo bem?

Uma função interna tem acesso a atributos da função envolvente

Uma função interna pode usar atributos da função externa, a função de escopo superior. Assim, mudamos a função get_mensagem() para receber o parâmetro nome recebido pela função compor_cumprimento(), retornando get_mensagem().

def compor_cumprimento(nome):
    def get_mensagem():
        return "Olá " + nome + "! Tudo bem?"
    return get_mensagem

cumprimentar = compor_cumprimento("Francisco")
print(cumprimentar())
Olá Francisco! Tudo bem?

Quando, como no exemplo acima, estão presentes os itens abaixo:
  • uma função aninhada (função dentro de uma função)
  • a função aninhada usando um valor definido na função de inclusão
  • a função de inclusão retornar a função interna, aninhada
Temos o que chamamos de closure, encerramento, (https://www.programiz.com/python-programming/closure). Um padrão muito encontrado durante a construção de decoradores.
Outro ponto é que Python só permite o acesso de leitura ao escopo externo e não atribuição. A não ser que isso seja declarado de forma explícita usando a palavra chave nonlocal (https://www.programiz.com/python-programming/keyword-list#nonlocal).

Compondo nosso primeiro decorador

Usando as ideias acima podemos criar nosso primeiro decorador. Vamos usar uma função que retorna uma frase de bem_vindo() e ampliá-la colocando esse texto numa tag html p

def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print(bem_vindo('Francisco'))
Olá Francisco, seja bem vindo ao nosso site!
def p_decorate(func):
    def func_modificada(nome):
        return "<p>{}</p>".format(func(nome))
    return func_modificada

my_bem_vindo = p_decorate(bem_vindo)

print(my_bem_vindo("Francisco"))
<p>Olá Francisco, seja bem vindo ao nosso site!</p>

Esse foi o nosso primeiro decorador. Uma função que recebe outra como argumento, gera uma nova função aumentando o trabalho da função original e retorna a função gerada para que possamos usá-la em qualquer lugar.

Para que bem_vindo() seja decorado por p_decorate() podemos redefini-la passando-a dentro do próprio decorador

def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)
bem_vindo = p_decorate(bem_vindo)
print(bem_vindo('Francisco'))
<p>Olá Francisco, seja bem vindo ao nosso site!</p>

Outra observação é que, como nossa função decorada recebe o argumento "nome", tudo o que precisamos fazer no decorador é permitir que o invólucro de bem_vindo() receba e passe esse argumento para a função original.

Sintaxe do decorador em Python

Python torna a criação e o uso de decoradores um agradável para o programador através de um açúcar sintático. Para decorar bem_vindo não temos que usar bem_vindo = p_decorator(bem_vindo). Há um atalho para isso, que é mencionar o nome da função decoradora antes da função a ser decorada com um símbolo @. Neste caso: @p_decorate.

def p_decorador(func):
    def func_modificada(nome):
        return "<p>{}</p>".format(func(nome))
    return func_modificada

@p_decorador
def bem_vindo(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print(bem_vindo('José'))
<p>Olá José, seja bem vindo ao nosso site!</p>
Agora sim, nosso "decorator" ficou com uma sintaxe mais interessante.


Jupyter notebook contendo o código deste estudo:
github.com/FranciscoACLima/Python-decorators

Mais sobre o uso de decoradores em Python:
thecodeship.com/patterns/guide-to-python-function-decorators/

Continua em:
Decorators em Python - parte 2

Até...
Francisco ACLima

Nenhum comentário:

Postar um comentário