Хотя технически вы можете делать практически все с функциями и декораторами в 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, я могу порекомендовать вам прочитать о том, как это сделать в Зеленое дерево Змеи .