Изменить код функции с помощью декоратора и выполнить его с помощью eval? - PullRequest
0 голосов
/ 27 октября 2019

Я пытаюсь применить декоратор, который изменяет код функции, а затем выполнить эту функцию с измененным кодом.

Ниже приведен модуль temp с примером функции. Я просто хочу, чтобы функция возвращала [*args, *kwargs.items(), 123] вместо [*args, *kwargs.items()], если к этой функции применяется some_decorator.

Редактировать: обратите внимание, что это всего лишь игрушечный пример, я не собираюсь добавлятьновые значения в списке, но лучше переписать большую часть функции.

from inspect import getsource

def some_decorator(method):
    def wrapper(*args, **kwargs):
        source_code = getsource(method)
        code_starts_at = source_code.find('):') + 2
        head = source_code[:code_starts_at]
        body = source_code[code_starts_at:]

        lines = body.split('\n')
        return_line = [i for i in lines if 'return' in i][0]
        old_expr = return_line.replace('    return ', '')
        new_expr = old_expr.replace(']', ', 123]')

        new_expr = head + '\n' + '    return ' + new_expr

        return eval(new_expr)
    return wrapper

@some_decorator
def example_func(*args, *kwargs):
    return [*args, *kwargs]

Немного больше объяснений: я переписываю исходную функцию

def example_func(*args, **kwargs):
    return [*args, *kwargs.items()]

в

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]

Я надеюсь, что eval сможет скомпилировать и запустить эту измененную функцию.

Когда я пытаюсь запустить ее, она возвращает синтаксическую ошибку.

from temp import example_func
example_func(5)

ЯПомните, что eval может справиться с этим:

[*args, *kwargs.items(), 123]

, но только если args и kwargs уже объявлены. Я хочу, чтобы они читались из example_func(args, kwargs), когда я выполняю example_func.

Я полагаю, что просто записать измененный код функции в файл

def example_func(*args, **kwargs):
    return [*args, *kwargs.items(), 123]

и сделать some_decoratorвыполнить функцию с измененным кодом вместо исходного, будет работать нормально. Однако в идеале я бы пропустил создание каких-либо промежуточных файлов.

Можно ли этого добиться?

1 Ответ

3 голосов
/ 27 октября 2019

Хотя технически вы можете делать практически все с функциями и декораторами в Python, вы не должны .

В этом конкретном случае добавление дополнительного значения в функцию, которая возвращаетсписок так же прост, как:

def some_decorator(method):
    def wrapper(*args, **kwargs):
        result = method(*args, **kwargs)
        return result + [123]
    return wrapper

Это не требует переписывания кода функции. Если все, что вы делаете, это изменяете входы или выходы функции, просто изменяйте входы или выходы и оставляйте саму функцию такой.

Декораторы в основном просто синтаксическиесахар здесь, способ изменить

def function_name(*args, **kwargs):
    # ...

function_name = create_a_wrapper_for(function_name)

на

@create_a_wrapper_for
def function_name(*args, **kwargs):
    # ...

Также обратите внимание, что eval() функция не может измените вашу функцию, потому что eval() строго ограничен выражениями . Синтаксис def для создания функции - это оператор . По сути, операторы могут содержать выражения и другие операторы (например, if <test_expression>: <body of statements>), но выражения не могут содержать операторы. Вот почему вы получаете исключение SyntaxError;в то время как является допустимым выражением, return [*args, *kwargs.items()] является оператором (содержащим выражение):


>>> args, kwargs = (), {}
>>> eval("[*args, *kwargs.items()]")
[]
>>> eval("return [*args, *kwargs.items()]")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    return [*args, *kwargs.items()]
         ^
SyntaxError: invalid syntax

Чтобы выполнить текст как произвольный код Python, вам нужно будет использовать exec()вместо этого используйте функцию и позаботьтесь об использовании того же пространства имен, что и у исходной функции, чтобы все глобальные переменные, используемые в исходной функции, все еще были доступны.

Например, если функция вызывает другую функцию для получения дополнительнойзначение:

def example(*args, **kwargs):
    return [extra_value(), *args, *kwargs.items()]

def extra_value():
    return 42

, тогда вы не можете выполнить функцию example() изолированно;он является частью глобального пространства имен модуля и ищет extra_value в этом пространстве имен при вызове функции. Функции имеют ссылку на глобальное пространство имен модуля, в котором они созданы, и доступны через атрибут function.__globals__. Когда вы используете exec() для выполнения оператора def, создающего функцию, тогда новый объект функции подключается к глобальному пространству имен, которое вы передали. Обратите внимание, что def создает объект функции и присваивает его имени функции, поэтомувам придется снова извлечь этот объект из того же пространства имен:

>>> namespace = {}
>>> exec("def foo(): return 42", namespace)
>>> namespace["foo"]
<function foo at 0x7f8194fb1598>
>>> namespace["foo"]()
42
>>> namespace["foo"].__globals__ is namespace
True

Далее, манипулирование текстом для восстановления кода Python очень неэффективно и подвержено ошибкам. Например, ваш код str.replace() потерпит неудачу, если функция будет использовать это вместо:

def example(*args, **kwargs):
    if args or kwargs:
        return [
            "[called with arguments:]",
            *args,
            *kwargs.items()
        ]

, поскольку теперь return имеет отступ, в строке есть значение [..] в строковом значении, изаключительная скобка списка ] находится на отдельной строке.

Было бы гораздо лучше, если бы исходный код Python компилировался в Абстрактное синтаксическое дерево (через ast модуль ), затем работа над этим деревом. Направленный граф четко определенных объектов гораздо проще манипулировать, чем текст (который гораздо более гибок в отношении того, сколько пробелов используется и т. Д.). И приведенный выше код, и ваш пример приведут к созданию дерева с узлом Return(), содержащим выражение, верхним уровнем которого будет узел List(). Вы можете пройти по этому дереву и найти все Return() узлы и изменить их List() узлы, добавив дополнительный узел в конец содержимого списка.

Python AST может быть скомпилирован в объект кода (с помощьюcompile()) затем выполните exec() (который принимает не только текст, но и объекты кода).

Для реального примера проекта, который переписывает код Python,Посмотрите , как pytest переписывает оператор assert, чтобы добавить дополнительный контекст . Для этого они используют ловушку для импорта модуля, но пока для функции доступен исходный код, вы можете делать это и с декоратором.

Вот пример использования модуля ast дляизмените список в операторе return, добавив произвольную константу:

import ast, inspect, functools

class ReturnListInsertion(ast.NodeTransformer):
    def __init__(self, value_to_insert):
        self.value = value_to_insert

    def visit_FunctionDef(self, node):
        # remove the `some_decorator` decorator from the AST
        # we don’t need to keep applying it.
        if node.decorator_list:
            node.decorator_list = [
                n for n in node.decorator_list
                if not (isinstance(n, ast.Name) and n.id == 'some_decorator')
            ]
        return self.generic_visit(node)

    def visit_Return(self, node):
        if isinstance(node.value, ast.List):
            # Python 3.8 and up have ast.Constant instead, which is more
            # flexible.
            node.value.elts.append(ast.Num(self.value))
        return self.generic_visit(node)

def some_decorator(method):
    source_code = inspect.getsource(method)
    tree = ast.parse(source_code)

    updated = ReturnListInsertion(123).visit(tree)
    # fix all line number references, make it match the original
    updated = ast.increment_lineno(
        ast.fix_missing_locations(updated),
        method.__code__.co_firstlineno
    )

    ast.copy_location(updated.body[0], tree)

    # compile again, as a module, then execute the compiled bytecode and
    # extract the new function object. Use the original namespace
    # so that any global references in the function still work.
    code = compile(tree, inspect.getfile(method), 'exec')
    namespace = method.__globals__
    exec(code, namespace)
    new_function = namespace[method.__name__]

    # update new function with old function attributes, name, module, documentation
    # and attributes.
    return functools.update_wrapper(method, new_function)

Обратите внимание, что для этого не требуется функция-обертка . вам не нужно переделывать функцию каждый раз, когда вы пытаетесь вызвать ее, декоратор может сделать это только один раз и вернуть полученный объект функции напрямую.

Вот демонстрационный модуль, с которым можно попробовать:

@some_decorator
def example(*args, **kwargs):
    return [extra_value(), *args, *kwargs.items()]

def extra_value():
    return 42

if __name__ == '__main__':
    print(example("Monty", "Python's", name="Flying circus!"))

Вышеуказанные выходы [42, 'Monty', "Python's", ('name', 'Flying circus!'), 123] при запуске.

Однако гораздо проще использовать первый метод.

Если вы хотите продолжить использовать exec() и манипуляции с AST, я могу порекомендовать вам прочитать о том, как это сделать в Зеленое дерево Змеи .

...