Decoradores em Python

decoradores_em_python-1_abertura

Um decorador em Python é um objeto que estende/modifica a funcionalidade de uma função (ou método) em tempo de execução e conceitualmente está mais próximo da anotação do Java que do decorador da orientação a objetos.

Na prática, o decorador age como uma embalagem de presente, acondicionando a função sem alterar seu conteúdo (ele continua sendo um presente) mas deixando-o mais bonito.

Como funciona…

Alguns conceitos importantes antes de começar…

Funções são objetos

É bom lembrar que em Python as funções também são objetos…

#!/usr/bin/env python
from __future__ import print_function

def minha_funcao(i, j):
    return i,j

if __name__ == "__main__":
    print("*** minha_funcao() ***")
    print(minha_funcao(2, 3))

    outra_funcao = minha_funcao

    print("*** outra_funcao() ***")
    print(outra_funcao(2, 3))

resultará em…

$ python ./funcao_como_objeto.py
*** minha_funcao() ***
5
*** outra_funcao() ***
5

Mesmo que outra_funcao() não tenha sido explicitamente declarada dentro do código ela funciona tal qual minha_funcao() já que faz referência ao mesmo objeto.

Funções são argumentos

Elas podem ser passadas para outras funções como se fossem argumentos

#!/usr/bin/env python
from __future__ import print_function

def minha_funcao(i, j):
    return i + j

def outra_funcao(func, *args, **kwargs):
    return func(*args, **kwargs) * 2

def main():
    print("*** minha_funcao() ***")
    print(minha_funcao(2, 3))

    print("*** outra_funcao() ***")
    print(outra_funcao(minha_funcao, 2, 3))

if __name__ == "__main__":
    main()

terá como resultado…

$ python ./funcao_como_argumento.py
*** minha_funcao() ***
5
*** outra_funcao() ***
10

Neste caso minha_funcao() é passada como argumento para outra_funcao() que a executa dentro dela, duplica o valor obtido e retorna o resultado.

Assim é possível juntar os dois conceitos e fazer o seguinte…

#!/usr/bin/env python
from __future__ import print_function

def decora_funcao(func):
    def decoracao():
        print("%%% agora decorada %%%")
        func()

    return decoracao

def main():
    def minha_funcao():
        print("### minha_funcao() ###")

    minha_funcao = decora_funcao(minha_funcao)
    minha_funcao()

if __name__ == "__main__":
    main()

para obter…

$ python funcao_decorada_como_argumento.py
%%% agora decorada %%%
### minha_funcao() ###

Dois detalhes, aqui:

  • A minha_funcao() foi colocada dentro de main() para que o Python não ficasse reclamando e
  • Para simplificar, removi a passagem de argumentos mas a lógica continua a mesma.

E assim uma função é “embrulhada” (decorada) dentro da outra.

Simplificando…

Acontece que a linha onde a minha_funcao() é recebe a decoração por decora_funcao()…

    ...
    minha_funcao = decora_funcao(minha_funcao)
    ...

…pode ser simplificada através da sintaxe do Python como…

    ...
    @decora_funcao
    def minha_funcao():
        print("### minha_funcao() ###")
    ...

para obter o mesmo resultado…

$ python funcao_decorada.py 
%%% agora decorada %%%
### minha_funcao() ###

E esta é uma função decorada! 🙂

Um exemplo prático

Agora com um exemplo bem mais prático, começando com a criação de função cujo objetivo é transformar um dicionário em uma tabela.

def tabulate_data(data_dict, sort_keys=True, sort_data=False):

    table_values = []
    temp_header_set = set()

    for i in data_dict:
        __ = [temp_header_set.add(j) for j in i.keys()]

    table_header = list(sorted(temp_header_set) if sort_keys else temp_header_set)

    for j in data_dict:
        table_values.append([j.get(keys, "") for keys in table_header])

    if sort_data:
        table_values.sort()

    return [table_header] + table_values

O que ela faz é receber um dicionário, por exemplo um arquivo JSON, e transformá-lo em uma tabela.

Um pouco mais de código para testá-la…

from __future__ import print_function
from json import loads as json_loads
...
def main():
    notas_json = json_loads(open(NOTAS_JSON, "r").read())
    dados_alunos = tabulate_data(notas_json)
    print(dados_alunos)

if __name__ == "__main__":
    main()

Cujo resultado é este, uma lista contendo listas:

$ python testa_decoradores.py
[['id', 'nome', 'nota_1', 'nota_2', 'nota_3', 'nota_4'], [1, 'Adão 
Nogueira', 9.89, 8.75, 6.34, 8.05], [2, 'Bruno Tavares', 6.32, 8.25, 
7.67, 7.97], [3, 'João da Silva', 5.1, 5.22, 7.33, 8], [4, 'José 
Queiroz', 9.31, 7.9, 8, 8.832]]

Sim, o arquivo JSON usado aqui é o mesmo também utilizado no exemplo em Angular.

Claro que esta saída pode ser melhorada para obter uma saída um pouco mais legível como, por exemplo, HTML

def html(func_obj):

    def html_format(*args, **kwargs):
        table_data = func_obj(*args, **kwargs)

        table_header = format_line(" <th>{}</th>", table_data.pop(0), "\n")
        table_body = " <tbody>\n"

        for line in table_data:
            table_body += (
                " <tr>\n" + format_line(" <td>{}</td>", line, "\n") + "\n </tr>\n"
            )

        table_body += " </tbody>"

        return "<table>\n <thead>\n <tr>\n{}\n </tr>\n </thead>\n{}\n</table>".format(
            table_header, table_body
        )

    return html_format

De forma bem resumida o que esta função faz é iterar linha a linha com a lista produzida pela função tabulate_data() — outra função que produza o mesmo tipo de saída — para construir uma tabela em HTML com todos os seus componentes.

Daí é só decorar a função tabulate_data()…

from __future__ import print_function
from json import loads as json_loads
from decoradores import html
...
@html
def tabulate_data(data_dict, sort_keys=True, sort_data=False):
    ...
...

e o resto fica por conta da linguagem…

$ python testa_decoradores.py > notas.html

decoradores_em_python-1_tabela

Preferi colar a tabela já formatada em HTML do que uma “tripa” de tags aqui… 🙂

Concluindo…

Os arquivos utilizados nos exemplos acima estão no repositório git do blog e em versões com mais comentários no código explicando o que está acontecendo. Na versão do repositório a função tabulate_data() em “testa_decoradores.py” não recebeu decorador, esta parte ficará para você.

Aliás, além do @html, há um outro decorador, o @markdown, que formatará os dados de tabulate_data() em, claro, markdown!

Até!

Anúncios