sexta-feira, 22 de dezembro de 2017

Decorators em Python - parte 3


Decorando métodos e passando parâmetros nos decorators

Continuação de Decorators em Python - parte 2 

Decorando Métodos

Em Python, métodos são funções que esperam que seu primeiro parâmetro seja uma referência ao objeto atual. Métodos são funções dentro de classes. Podemos criar decoradores para métodos da mesma maneira que criamos para uma função simples. Vamos usar o p_decorate usado nos nossos exemplos anteriores para decorar uma função que retorna o nome completo de uma pessoa.

def p_decorate(func):
   def func_wrapper(self):
       return "<p>{}</p>".format(func(self))
   return func_wrapper

class Pessoa(object):
    def __init__(self, nome, sobrenome):
        self.nome = nome
        self.sobrenome = sobrenome

    @p_decorate
    def get_nome_completo(self):
        return self.nome + " " + self.sobrenome

pessoa = Pessoa('Francisco', 'Lima')
print(pessoa.get_nome_completo())
<p>Francisco Lima</p>

Passando argumentos nos decoradores

Uma abordagem bem melhor seria tornar nosso decorador útil para funções e métodos. Isso pode ser feito colocando *args e *kwargs como parâmetros para o wrapper, então ele pode aceitar qualquer número arbitrário de argumentos e argumentos de palavras-chave.

def p_decorate(func):
    def func_wrapper(*args, **kwargs):
       return "<p>{0}</p>".format(func(*args, **kwargs))
    return func_wrapper

class Pessoa(object):
    def __init__(self, nome, sobrenome):
        self.nome = nome
        self.sobrenome = sobrenome

    @p_decorate
    def get_nome_completo(self):
        return self.nome + " " + self.sobrenome

pessoa = Pessoa('Francisco', 'Lima')
print(pessoa.get_nome_completo())
<p>Francisco Lima</p>

Vamos ver novamente o exemplo dos 3 decoradores, p, strong e div que usamos na parte 2 deste Post.

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

def strong_decorate(func):
    def func_wrapper(name):
        return "<strong>{0}</strong>".format(func(name))
    return func_wrapper

def div_decorate(func):
    def func_wrapper(name):
        return "<div>{0}</div>".format(func(name))
    return func_wrapper

@div_decorate
@p_decorate
@strong_decorate
def get_text(nome):
    return "Olá {0}, seja bem vindo ao nosso site!".format(nome)
print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Olhando para ele, podemos notar que os 3 decoradores, div_decorate, p_decorate, strong_decorate, são redundantes, cada um possuí basicamente a mesma funcionalidade, apenas envolvendo a string com diferentes tags.
Dá para fazer muito melhor do que isso. Por que não ter uma implementação mais geral para um decorator que leva a tag para envolver a string? Pois é isso que vamos fazer.

def tags(tag_name):
    def tags_decorator(func):
        def func_wrapper(name):
            return "<{0}>{1}</{0}>".format(tag_name, func(name))
        return func_wrapper
    return tags_decorator

@tags("div")
@tags("p")
@tags("strong")
def get_text(nome):
    return "Olá {0}, seja bem vindo ao nosso site!".format(nome)

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

Usando um único decorator, substituimos os 3 anteriores. É claro que tivemos um pouco mais de trabalho neste caso. Os decoradores esperam receber uma função como argumento, e é por isso que tivemos que construir uma função que receba esses argumentos extras para gerar o nosso decorador sobre ela.
No exemplo acima, @tags , é o nosso gerador do decorador.

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/

Até...
Francisco ACLima

quinta-feira, 14 de dezembro de 2017

Decorators em Python - parte 2

Aplicando mais de um decorador numa função

No exemplo abaixo, temos a função get_text decorada com @p_decorate que envolve o texto numa tag p.

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

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

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

Agora, considerem que queiramos decorar nossa função get_text com outras 2 funções para envolver uma div e uma label strong em torno da saída do paragráfo.

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

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

def strong_decorate(func):
    def func_wrapper(name):
        return "<strong>{0}</strong>".format(func(name))
    return func_wrapper

def div_decorate(func):
    def func_wrapper(name):
        return "<div>{0}</div>".format(func(name))
    return func_wrapper

Com a abordagem básica, decorar get_text() seria com a linha de comando abaixo:

get_text = div_decorate(p_decorate(strong_decorate(get_text)))
print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Com a sintaxe do decorador do Python, a mesma coisa pode ser alcançada de uma forma mais clara:

@div_decorate
@p_decorate
@strong_decorate
def get_text(nome):
    return "Olá {}, seja bem vindo ao nosso site!".format(nome)

print (get_text("John"))
<div><p><strong>Olá John, seja bem vindo ao nosso site!</strong></p></div>

Uma coisa importante a observar é que a ordem dos nossos decoradores é importante. O primeiro decorador é o último a ser aplicado.
Se, por exemplo, colocarmos @strong_decorate em primeiro, todo o texto restante é inserido dentro da tag strong.

@strong_decorate
@div_decorate
@p_decorate
def get_text(nome):
    return "lorem ipsum, {0} dolor sit amet".format(nome)

# Basicamente, o que está sendo feito com o decorador é:
# get_text = div_decorate(p_decorate(strong_decorate(get_text)))

print (get_text("John"))
<strong><div><p>Olá John, seja bem vindo ao nosso site!</p></div></strong>


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 3

Até...
Francisco ACLima

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