Lista de Conteúdos

Introdução

Não se assuste com as palavras no título. Embora possam ser estranhas para você provavelmente em algum momento você utilizou ferramentas que fazem uso de técnicas de metaprogramação ou inspeção de AST. Pytest e Numba são exemplos.

No post anterior eu falei sobre python frames e inspection.. Mostrei como podemos usar inspect.signautre para criar um decorador que valide argumentos:

@math_validator() 
def simple_method(x: "\in R", y: "\in R_+", z: float = 2) -> float: 
    ... 
simple_method(1, 0) 
simple_method((1, 2)) -> 1.5 
---> 19 simple_method(1, 0) 
... 
<locals>.decorate.<locals>.decorated(*_args) 
     11         continue 
     13     if not MATH_SPACES[annotation]["validator"](_args[i]): 
---> 14         raise ValueError(f"{k} doesn't belong to the {MATH_SPACES[annotation]['name']}") 
     15 result = func(*_args) 
     16 print(f"{func.__name__}({_args}) -> {result}") 

ValueError: y doesn't belong to the space of real numbers greater than zero 

No outro exemplo mostrei como podemos combinar o signature com sys.trace para criar um decorador que expõe o locals da função decorada. O que nos permite fazer coisas legais tais como criar um decorador @report

@report('{arg.n_bananas} Monkey {gluttonous_monkey} ate too much bananas.  Num monkeys {num_monkeys}') 
def feed_monkeys(n_bananas):  
    num_monkeys = 3 
    monkeys = { 
        f"monkey_{i}": {"bananas": 0} 
        for i in range(num_monkeys) 
    } 
    while n_bananas > 0: 
        if np.random.uniform() < 0.4: 
            continue 

        monkey = monkeys[np.random.choice(list(monkeys.keys()))] 
        if n_bananas > 0: 
            monkey["bananas"] += 1 
            n_bananas -= 1 

    gluttonous_monkey = max(monkeys, key=lambda k: monkeys[k]["bananas"])  

Contudo, no final do post passado eu disse que essa solução tem alguns problemas

import sys 
import inspect 
from types import SimpleNamespace 


def call_and_extract_frame(func, *args, **kwargs): 
    frame_var = None 
    trace = sys.gettrace() 
    def update_frame_var(stack_frame, event_name, arg_frame): 
        """ 
        Args: 
            stack_frame: (frame) 
                The current stack frame. 
            event_name: (str) 
                The name of the event that triggered the call.  
                Can be 'call', 'line', 'return' and 'exception'. 
            arg_frame:  
                Depends on the event. Can be a None type 
        """ 
        nonlocal frame_var # nonlocal is a keyword which allows us to modify the outisde scope variable 

        if event_name != 'call': 
            return trace 

        frame_var = stack_frame 
        sys.settrace(trace) 
        return trace 

    sys.settrace(update_frame_var) 
    try: 
        func_result = func(*args, **kwargs) 
    finally: 
        sys.settrace(trace) 
    return frame_var, func_result 

def report(formater): 
    def decorate(func): 
        def decorated(*_args): 
            sig = inspect.signature(func) 
            named_args = {} 
            num_args = len(_args) 
            for i, (k, v) in enumerate(sig.parameters.items()): 
                if i < num_args: 
                    named_args[k] = repr(_args[i]) 
                else: 
                    named_args[k] = repr(v.default) 
            frame_func, _result = call_and_extract_frame(func, *_args) 
            name = func.__name__ 
            result = repr(_result) 
            args_dict = { 
                "args": SimpleNamespace(**named_args),  
                "args_repr": repr(SimpleNamespace(**named_args)), 
                **locals(), 
                **frame_func.f_locals, 
            } 
            print(formater.format(**args_dict)) 
            # do other stuff here 
            return _result  
        return decorated 
    return decorate 

Quais são os problemas?

  • É esperado que o tracing reduza a performance do sistema. Se você usar a solução acima só para casos pontuais ou debug é ok

  • Pode criar conflitos com outras ferramentas e bibliotecas que também estão usando a ferramenta de tracing, tais como debuggers.

  • Parece uma solução feia!

Você pode se perguntar: “Overengineering! Era só esse fazer isso aqui:

@report('stuff goes here') 
def func(x, y): 
    random_var = np.random.uniform() 
    ... #more local vars 
    result = (x+y)**random_var 
    return result, locals  

"..e dentro do decorador ele mudar para isso"

_result, local_vars = func(x, y) 

A razão é:

O ponto de usar um decorador é para evitar mudanças em qualquer outra parte da nossa codebase. Por exemplo, se em qualquer outra parte da nossa codebase func está sendo chamada, eu teria que fazer mudanças do tipo

result = func(x, y) # to  
result = func(x, y)[0] 

E se futuramente eu quisesse remover o decorador de uma função eu teria que desfazer todas as mudanças acima

  • Você irá aumentar o cognitive load de todos os membros do seu time que não precisam saber sobre ou usar o decorador.

  • Se você está ok com fazer mudanças em outros lugares do seu código por que não criar novas funções ao invés de decoradores que funcionam mais ou menos?

Ok, você pode estar pensando: “Tá , faz sentido não fazer isso que sugeri, mas do que adianta evitar sujar sua codebase se você tá criando problemas de desempenho e debug? Não parece uma boa solução na maioria dos casos.” Eu tenho que concordar com você!

Bom, então o que podemos fazer?? O problema que encontramos é que em python não temos context managers que podem lidar com namespaces https://mail.python.org/archives/list/python-ideas@python.org/. Mas não se desanime com essa limitação, a questão agora é:

Se uma linguagem não tem uma feature que eu preciso o que eu posso fazer?

Em pyhton estamos bem com isso pois é fácil manipular o que é conhecido como Abstract Syntax Tree (árvore sintática abstrata) e compilar ela em uma nova função em tempo de execução (runtime). ** Quando programamos desse jeito estamos no reino da metaprogramação! Tentarei esclarecer esses pontos agora**

ASTs: O que são?

Uma linguagem de programação é obviamente, pelo menos uma linguagem… OK, mas o que é uma linguagem? Todas as linguagens humanas compartilham uma estrutura em comum? Como podemos comparar sentenças diferentes na mesma linguagem? Essas questões talvez pareçam ser mais adequadas para serem respondidas por filósofos. Contudo, também é tema de trabalho de matemáticos e computeiros

A grande diferença é que matemáticos e computeiros comumente preferem falar sobre coisas usando algum formalismo matemático. Em essência, AST faz parte de um formalismo matemático que permite isso. Uma AST permite representar uma sentença através de um grafo direcionado do tipo árvore. Para isso usamos um conjunto de regras bem definidas em como construir essa árvore.

Como saber se uma sentença está gramaticalmente correta?

Você provavelmente se lembra quase institivamente de um conjunto de regras que aprendeu durante sua vida ou acabou se acostumando sobre como organizar e compor verbos, substantivos, adjetivos, etc. Este conjunto de regras e guias é a sintaxe da linguagem que você fala/escreve. ASTs permitem checar e compreender uma sentença utilizando essas regras

Pegue por exemplo a sentença

“I drive a car to my college”, a AST é a seguinte

Fonte: Geeks for Geeks:Syntax Tree – Natural Language Processing.

Qual a vantagem de usar ASTs? Note que não precisamos falar de espaços, caligrafia ou estilo pessoal de organizar escrita para compreender uma sentença e saber se ela está válida. Além disso, temos uma estrutura hierárquica que permite entender a sentença por níveis!

Não é uma surpresa que ASTs são também uma ferramenta comum em processos de analisar a validade de um código ou na construção de um compilador/interpretador. Nesse post iremos manipular a AST! Mas antes disso quero fazer uma pergunta:

Python é interpretado ou compilado?

Geralmente, quando encontro um hater de python ou mesmo um entusiasta ouço ou leio coisas do tipo:

  • Python é lento pois é uma linguagem interpretada
  • “*Python é legal pois não tem chatice de compilação”
  • “Python é ruim comparado a C pois não tem um compilador”

Bem, essas asserções não são verdadeiras, pois estão usando conceitos errados! Outra confusão é que geralmente quando se fala em python estamos nos referindo a linguagem (sintaxe, etc) python mais a máquina virtual do CPython. Vamos conversar um pouco mais sobre isso

Dizer que uma linguagem hoje é puramente compilada ou interpretada é confuso, pois essa divisão é borrada. Veja o seguinte

hello_world = "print('Hello, world!')"
hello_world_obj = compile(hello_world, '<string>', 'single')

Pois é… se você tentaria defender nos comentários que python é puramente interpretado as coisas estão mais difíceis para você. Por que tem um compile disponível? O que ele faz?

exec(hello_world_obj)
Hello, world!

O que será que tem dentro desse hello_world_obj?

print(f"Bad news for you:\n\tContent: {hello_world_obj.co_code}\n\tType: {type(hello_world_obj.co_code)}")

    Bad news for you:
    	Content: b'e\x00d\x00\x83\x01F\x00d\x01S\x00'
    	Type: <class 'bytes'>

Para entender os prints acima você precisa compreender o que acontece por trás dos panos quando um código python é “interpretado”.

Após você escrever um código e chamar o comando python, o python inicia um processo de compilação criando as ASTs, depois gerando bytecodes a partir das ASTs e esses últimos serão encapsulados em code_objects. Na última etapa os code objects serão interpretados pela máquina virtual do CPython. O diagrama à baixo é uma representação simples (com passos omitidos) do processo

graph LR; A[Source Code]-->|parsing|B[Parse Tree]; B-->C[AST]; C-->E[Bytecode]; E-->F[Code Object]; F-->|execution by|G[CPython Virtual Machine];

A fase de compilação são os primeiros passos do diagrama acima

graph LR; A[Source Code]-->|parsing|B[Parse Tree]; B-->C[AST]; C-->E[Bytecode]; E-->F[Code Object];
Se você não conhece os conceitos dos nomes acima não se preocupe, não precisamos de tanto aprofundamento. **Bytecodes são apenas uma maneira compacta de dizer ao interpretador o que o código quer que ele faça. Enquanto code objects são coisas que encapsulam esses bytecodes.**

Ok, onde isso entra na minha solução? O que eu proponho fazer é manipular a AST e compilar um novo code object que será interpretado pelo cpython!

Extraindo e interpretando ASTs

Veja o seguinte exemplo:

import inspect
import ast
import astor # install this for pretty printing
def example(a: float, b:float = 2) -> float:
    s = a+b
    return s

tree = ast.parse(inspect.getsource(example))
print(astor.dump(tree))
astor.to_source(tree)
Module(
    body=[
        FunctionDef(name='example',
            args=arguments(posonlyargs=[],
                args=[arg(arg='a', annotation=Name(id='float'), type_comment=None),
                    arg(arg='b', annotation=Name(id='float'), type_comment=None)],
                vararg=None,
                kwonlyargs=[],
                kw_defaults=[],
                kwarg=None,
                defaults=[Constant(value=2, kind=None)]),
            body=[
                Assign(targets=[Name(id='s')],
                    value=BinOp(left=Name(id='a'), op=Add, right=Name(id='b')),
                    type_comment=None),
                Return(value=Name(id='s'))],
            decorator_list=[],
            returns=Name(id='float'),
            type_comment=None)],
    type_ignores=[])

O output acima é a AST da função. Gaste algum tempo olhando essa saída e tente entender/inferir o que cada coisa significa e como ela é organizada. A imagem abaixo é a representação visual da saída acima

Cada elemento do output que inicia com uma letra maiúscula é um nó, node(Name, BinOp, FunctionDef, etc) derivado da classe ast.Node. Um dos nós mais importante é o ast.Name. Por exemplo em

value=BinOp(left=Name(id='a'), op=Add, right=Name(id='b')),

o ast.Name(... é usado para referenciar as variáveis a e b da nossa função.

Ok, voltemos ao nosso problema. Lembre-se que uma solução ruim era reescrever cada função que precisa ser decorada, por exemplo

def func(x, y):
    random_var = np.random.uniform()
    ... #more local vars
    result = (x+y)**random_var
    return result

como

def func_transformed(x, y):
    random_var = np.random.uniform()
    ... #more local vars
    result = (x+y)**random_var
    return result, locals 

A coisa legal que faremos aqui é escrever uma função que escrevera essas mudanças para nós! E depois colocaremos a compilação dentro de um decorador para evitar que nossa codebase seja alterada.

Como metaprogramar de forma eficiente?

Fazer um código que faça as alterações desejadas na nossa AST pode ser trabalhoso. Como começar a ter uma ideia do que precisa ser feito? Eu penso em uma sucessão de 6 passos e ir iterando para melhorar

6 passos simples

  1. Criar uma função exemplo (A)
  2. Codar uma função transformada do jeito que queremos que ela seja (B)
  3. Escrever um teste para que possa ser usado posteriormente para
    checar se nossa função transformada (B) bate com a função gerada pela meta-programação (C)
  4. Extrair a AST de A e B
  5. Comparar as ASTs. O que elas diferem? Anote as diferenças
    • Você pode usar a difflib do python para fazer isso
  6. Criar uma nova e mais complexa função exemplo (A) e repetir o processo até termos uma boa ideia das modificações necessárias na AST

Criando nossa meta-função

Primeira iteração

Começaremos escrevendo uma função incrivelmente simples

def example_1(x, y):
    internal_var  =  222
    result = (x+y)**internal_var
    return result
def example_1_expected(x, y):
    internal_var = 222
    result = (x+y)**internal_var
    return result, locals()

def test_meta_example_1(meta_func, x, y):
    expected_result, expected_locals = example_1_expected(x, y)
    result, locals_dict = meta_func(x, y)
    assert result == expected_result
    assert expected_locals == locals_dict

Agora usaseri a difflib para entender as diferenças entre as duas ASTs.

import difflib
from pprint import pprint

example_1_ast_str = astor.dump_tree(ast.parse(inspect.getsource(example_1)))
example_1_expected_str = astor.dump_tree(ast.parse(inspect.getsource(example_1_expected)))


pprint(
    list(
        difflib.unified_diff(example_1_ast_str.splitlines(), example_1_expected_str.splitlines(), n=0)
    )
)
['--- \n',
'+++ \n',
'@@ -3 +3 @@\n',
"-        FunctionDef(name='example_1',",
"+        FunctionDef(name='example_1_expected',",
'@@ -19 +19 @@\n',
"-                Return(value=Name(id='result'))],",
"+                Return(value=Tuple(elts=[Name(id='result'), "
"Call(func=Name(id='locals'), args=[], keywords=[])]))],"]

Com o output acima sabemos aogra que precisaremos mudar o seguinte nó na AST

Return(value=Name(id='result'))],

para isto

Return(value=Tuple(elts=[Name(id='result'), Call(func=Name(id='locals'), args=[], keywords=[])]))],

Como alterar nós na AST? Com a ajuda do NodeTransformer

O NodeTransformer

O ast.NodeTransformer nos permite criar objetos com uma interface de caminhante. O caminhante visitará cada Node da AST e durante cada visita ele pode remover, substituir, modificar ou adicionar Nodes. Após fazer essas alterações o caminhante pode continuar sua caminhada nos filhos do Node ou apenas parar.

Vamos iniciar criando uma classe derivada de ast.NodeTransformer

class ASTTransformer(ast.NodeTransformer):
    def visit_Return(self, node):

Se queremos interagir com um nó do tipo AlgumaCoisa precisamos sobrescrever o método visit_AlgumaCoisa. Portanto, como sabemos que precisamos mudar o Return iremos sobrescrever o visit_Return. Precisaremos criar também um nó para pegar o locals. Esse nó é o Call

class ASTTransformer(ast.NodeTransformer):
    def visit_Return(self, node):
        node_locals = ast.Call(
            func=ast.Name(id='locals', ctx=ast.Load()),
            args=[], keywords=[]
        )
        self.generic_visit(node)
        return node

Veja que usamos o nó Name para identificar a função locals. Agora, de acordo com o resultado do nosso diff o resultado do Return precisa ser uma nó do tipo Tuple

class ASTTransformer(ast.NodeTransformer):
    def visit_Return(self, node):
        node_locals = ast.Call(
            func=ast.Name(id='locals', ctx=ast.Load()),
            args=[], keywords=[]
        )
        new_node.value = ast.Tuple(
            elts=[
                node.value,
                node_locals
            ],
            ctx=ast.Load()
        )
        self.generic_visit(new_node)
        return new_node

Uma nova coisa apareceu. O argumento elts. Não se preoucupe em entender tudo. Mas o elts é um arg que diz qual é a lista de nós que a Tupla deve conter. Toda vez que você quiser entender um pouco mais sobre ASTs e a gramática do python você pode consultar a documentação oficial aqui.

Quase tudo pronto. A última coisa que precisamos fazer é corrigir nossa AST. Pois ao alterar o Node precisamos preencher/corrigir as informações de line_number e column_offest. O python torna isso fácil com o método fix_missing_locations


class ASTTransformer(ast.NodeTransformer):
    def visit_Return(self, node):
        new_node = node
        node_locals = ast.Call(
            func=ast.Name(id='locals', ctx=ast.Load()),
            args=[], keywords=[]
        )
        new_node.value = ast.Tuple(
            elts=[
                node.value,
                node_locals
            ],
            ctx=ast.Load()
        )
        ast.copy_location(new_node, node)
        ast.fix_missing_locations(new_node)
        self.generic_visit(new_node)
        return new_node

Ok, vamos ver se funcionou. Para isso, precisamos instanciar nosso transformer e chamar o método visit que diz para o caminhante iniciar a caminhada e fazer as modificações pedidas

tree_meta = ast.parse(inspect.getsource(example_1))
transformer = ASTTransformer()
transformer.visit(tree_meta)
example_1_meta_ast_str = astor.dump_tree(tree_meta)
example_1_expected_str = astor.dump_tree(ast.parse(inspect.getsource(example_1_expected)))


pprint(
    list(
        difflib.unified_diff(example_1_meta_ast_str.splitlines(), example_1_expected_str.splitlines(), n=0)
    )
)
['--- \n',
'+++ \n',
'@@ -3 +3 @@\n',
"-        FunctionDef(name='example_1',",
"+        FunctionDef(name='example_1_expected',"]

Funcionou! Vamos adicionar um pouco mais de complicação para ver se o NodeTransformer continuará funcionando.

A segunda iteração

Seja criativo na hora de complicar, eu fiz isso aqui é feio mais adiciona muita confusão para estressar o NodeTransformer.

def example_2(x, y):
    internal_var  =  222
    def sub(x, y):
        ommit_this_var = 1
        return x - y
    result = sub(x,y)**internal_var
    return (result, False)
def example_2_expected(x, y):
    internal_var  =  222
    def sub(x, y):
        ommit_this_var = 1
        return x - y
    result = sub(x,y)**internal_var
    return ((result, False), locals())
def test_meta_example_2(meta_func, x, y):
    expected_result, expected_locals = example_2_expected(x, y)
    result, locals_dict = meta_func(x, y)
    del locals_dict["sub"]
    del expected_locals["sub"]
    assert result == expected_result
    assert expected_locals == locals_dict
example_2_ast_str = astor.dump_tree(ast.parse(inspect.getsource(example_2)))
example_2_expected_str = astor.dump_tree(ast.parse(inspect.getsource(example_2_expected)))


pprint(
    list(
        difflib.unified_diff(example_2_ast_str.splitlines(), example_2_expected_str.splitlines(), n=0)
    )
)
['--- \n',
'+++ \n',
'@@ -3 +3 @@\n',
"-        FunctionDef(name='example_2',",
"+        FunctionDef(name='example_2_expected',",
'@@ -37 +37,4 @@\n',
"-                Return(value=Tuple(elts=[Name(id='result'), "
'Constant(value=False, kind=None)]))],',
'+                Return(',
'+                    value=Tuple(',
"+                        elts=[Tuple(elts=[Name(id='result'), "
'Constant(value=False, kind=None)]),',
"+                            Call(func=Name(id='locals'), args=[], "
'keywords=[])]))],']

Agora é hora de cruzar os dedos e esperar que continue funcionando

tree_meta = ast.parse(inspect.getsource(example_2))
transformer = ASTTransformer()
transformer.visit(tree_meta)
example_2_meta_ast_str = astor.dump_tree(tree_meta)
example_2_expected_str = astor.dump_tree(ast.parse(inspect.getsource(example_2_expected)))


pprint(
    list(
        difflib.unified_diff(example_2_meta_ast_str.splitlines(), example_2_expected_str.splitlines(), n=0)
    )
)
['--- \n',
'+++ \n',
'@@ -3 +3 @@\n',
"-        FunctionDef(name='example_2',",
"+        FunctionDef(name='example_2_expected',",
'@@ -27,4 +27 @@\n',
'-                        Return(',
'-                            value=Tuple(',
"-                                elts=[BinOp(left=Name(id='x'), op=Sub, "
"right=Name(id='y')),",
"-                                    Call(func=Name(id='locals'), args=[], "
'keywords=[])]))],',
"+                        Return(value=BinOp(left=Name(id='x'), op=Sub, "
"right=Name(id='y')))],"]

Falhou miseravelmente. Qual é o problema? Se você olhar o diff com cuidado verá que o NodeTransformer alterou a função interna. Não queremos isso. Portanto, diremos para o caminhante evitar modificar se estiver em uma função interna. Para isso, precisamos sobrescrever o método visit_FunctionDef e criar uma flag para marcar em que nível o caminhante está

class ASTTransformer(ast.NodeTransformer):
    def visit_FunctionDef(self, node):
        if self._sub:
            return node
        self._sub = True
        self.generic_visit(node)
        return node

    def visit_Module(self, node):
        self._sub = 0
        self.generic_visit(node)

    def visit_Return(self, node):
        new_node = node
        node_locals = ast.Call(
            func=ast.Name(id='locals', ctx=ast.Load()),
            args=[], keywords=[]
        )
        new_node.value = ast.Tuple(
            elts=[
                node.value,
                node_locals
            ],
            ctx=ast.Load()
        )
        ast.copy_location(new_node, node)
        ast.fix_missing_locations(new_node)
        self.generic_visit(new_node)
        return new_node 
tree_meta = ast.parse(inspect.getsource(example_2))
transformer = ASTTransformer()
transformer.visit(tree_meta)
example_2_meta_ast_str = astor.dump_tree(tree_meta)
example_2_expected_str = astor.dump_tree(ast.parse(inspect.getsource(example_2_expected)))


pprint(
    list(
        difflib.unified_diff(example_2_meta_ast_str.splitlines(), example_2_expected_str.splitlines(), n=0)
    )
)
['--- \n',
'+++ \n',
'@@ -3 +3 @@\n',
"-        FunctionDef(name='example_2',",
"+        FunctionDef(name='example_2_expected',"]

Tudo ok! Próximo passo: compilar nossa ast.

Criando uma nova função em runtime

O que faremos agora é compilar a AST transformada e associa-la com uma nova função. Em python podemos fazer isso em tempo de execução com type.FunctionType

from types import FunctionType, CodeType

def transform_and_compile(func: FunctionType)->FunctionType:
    source = inspect.getsource(func)
    # we put this to remove the line from source code with the decorator
    source = "\n".join([l for l in source.splitlines() if not l.startswith("@")])
    tree = ast.parse(source)
    transformer = ASTTransformer()
    transformer.visit(tree)
    code_obj = compile(tree, func.__code__.co_filename, 'exec')
    function_code = [c for c in code_obj.co_consts if isinstance(c, CodeType)][0]
    # we must to pass the globals context to the function
    transformed_func = FunctionType(function_code, func.__globals__)
    return transformed_func
test_meta_example_1(transform_and_compile(example_1), 4, 2)
test_meta_example_2(transform_and_compile(example_2), 1, 2)

Veja que transform_and_compile foi capaz de criar novas funções que passaram nos testes que escrevemos nas iterações anteriores! Agora é o passo final e mais fácil desse post. Integrar com o decorador.

Integrando a manipulação de AST com um decorador

O que faremos é chamar transform_and_compile logo após o def decorate para evitar compilações desnecessárias toda vez que chamarmos a função decorada

def report(fmt):
    def decorate(func):
        meta_func = transform_and_compile(func)
        ....

Agora, dentro de def decorated podemos chamar a meta_func e retornar só o resultado pois não queremos mudar nossa codebase

def report(fmt):
    def decorate(func):
        meta_func = transform_and_compile(func)
        ...
        def decorated(*_args):
            _result, internal_locals = meta_func(*_args)
            ....
            return _result

Com todas as coisas que fizemos no post nosso decorador report está pronto para ser usado


def report(fmt):
    def decorate(func):
        meta_func = transform_and_compile(func)
        sig = inspect.signature(func)
        def decorated(*_args):
            _result, internal_locals = meta_func(*_args)
            named_args = {}
            num_args = len(_args)
            for i, (k, v) in enumerate(sig.parameters.items()):
                if i < num_args:
                    named_args[k] = repr(_args[i])
                else:
                    named_args[k] = repr(v.default)
            
            name = func.__name__
            result = repr(_result)
            args_dict = {
                **internal_locals,
                **locals(),
                **named_args
            }
            print(fmt.format(**args_dict))
            # store the information in some place
            return result
        return decorated 
    return decorate

Veja o resultado em uma função bem simples

@report(fmt='{name}(a={a}, b={b}, c={c}); sum_ab {sum_ab}, diff_ab {dif_ab}; r={result}')
def dummy_example(a, b, c=2):
    sum_ab = a + b
    dif_ab = a - b
    r = sum_ab**c + dif_ab**c
    return r

r = dummy_example(2, 3, 1)
print("r:", r)
    dummy_example(a=2, b=3, c=1); sum_ab 5, diff_ab -1; r=4
    r: 4

Eu sei que esse post pode ter sido difícil, se tiver dúvida você pode entrar em contato comigo pelos comentários abaixo, twitter ou linkedin. Compartilhe se você gostou.

Bruno Messias
Bruno Messias
Ph.D Candidate /Software Developer

Free-software enthusiast, researcher, and software developer. Currently, working in the field of Graphs, Complex Systems and Machine Learning.

comments powered by Disqus

Relacionados