Как убрать декораторы из функции в Python
Допустим, у меня есть следующее:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
Я хочу проверить spam
функция без необходимости устанавливать соединение (или что-то еще делает декоратор).
Дано spam
Как я могу отделить декоратор от него и получить базовую "неокрашенную" функцию?
10 ответов
В общем случае вы не можете, потому что
@with_connection
def spam(connection):
# Do something
эквивалентно
def spam(connection):
# Do something
spam = with_connection(spam)
Это означает, что "оригинальный" спам может даже больше не существовать. Взлом (не слишком симпатичный) был бы таким:
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
decorated._original = f
return decorated
@with_connection
def spam(connection):
# Do something
spam._original(testcon) # calls the undecorated function
Там было немного обновления для этого вопроса. Если вы используете Python 3, вы можете использовать __wrapped__
недвижимость для декораторов от stdlib.
Вот пример из Python Cookbook, 3-е издание, раздел 9.3 Развертывание декораторов
>>> @somedecorator
>>> def add(x, y):
... return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>
Если вы пытаетесь развернуть функцию из пользовательского декоратора, необходимо использовать функцию декоратора wraps
функция от functools
См. Обсуждение в Python Cookbook, 3-е издание, раздел 9.2 Сохранение метаданных функций при написании декораторов.
>>> from functools import wraps
>>> def somedecoarator(func):
... @wraps(func)
... def wrapper(*args, **kwargs):
... # decorator implementation here
... # ...
... return func(*args, kwargs)
...
... return wrapper
Решение Бальфы можно сделать более обобщенным с помощью этого мета-декоратора:
def include_original(dec):
def meta_decorator(f):
decorated = dec(f)
decorated._original = f
return decorated
return meta_decorator
Затем вы можете украсить свои декораторы @include_original, и у каждого будет спрятанная (недекорированная) версия, спрятанная внутри.
@include_original
def shout(f):
def _():
string = f()
return string.upper()
return _
@shout
def function():
return "hello world"
>>> print function()
HELLO_WORLD
>>> print function._original()
hello world
Вот, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:
orig_spam = spam.func_closure[0].cell_contents
Редактировать: Для функций / методов, оформленных более одного раза и с более сложными декораторами, вы можете попробовать использовать следующий код. Он основан на том факте, что декорированные функции отличаются от оригинальной функции __name__d.
def search_for_orig(decorated, orig_name):
for obj in (c.cell_contents for c in decorated.__closure__):
if hasattr(obj, "__name__") and obj.__name__ == orig_name:
return obj
if hasattr(obj, "__closure__") and obj.__closure__:
found = search_for_orig(obj, orig_name)
if found:
return found
return None
>>> search_for_orig(spam, "spam")
<function spam at 0x027ACD70>
Это не дурак, хотя. Он потерпит неудачу, если имя функции, возвращаемой декоратором, совпадает с именем декорированной. Порядок проверок hasattr() также является эвристическим, есть цепочки декораций, которые в любом случае возвращают неверные результаты.
Теперь вы можете использовать неокрашенный пакет:
>>> from undecorated import undecorated
>>> undecorated(spam)
Он проходит через копание всех слоев различных декораторов, пока не достигнет нижней функции и не требует замены оригинальных декораторов. Работает как на Python 2, так и на Python 3.
Рекомендуется украшать декораторов functools.wraps
вот так:
import functools
def with_connection(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
Начиная с Python 3.2, это автоматически добавит __wrapped__
атрибут, который позволяет вам получить оригинальную, неокрашенную функцию:
>>> spam.__wrapped__
<function spam at 0x7fe4e6dfc048>
Однако вместо ручного доступа к __wrapped__
атрибут, лучше использовать inspect.unwrap
:
>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>
Вместо того, чтобы делать...
def with_connection(f):
def decorated(*args, **kwargs):
f(get_connection(...), *args, **kwargs)
return decorated
@with_connection
def spam(connection):
# Do something
orig_spam = magic_hack_of_a_function(spam)
Вы могли бы просто сделать...
def with_connection(f):
...
def spam_f(connection):
...
spam = with_connection(spam_f)
... что все @decorator
синтаксис делает - вы можете, очевидно, получить доступ к оригиналу spam_f
обычно.
Оригинальная функция хранится в spam.__closure__[0].cell_contents
,
Декоратор использует замыкание, чтобы связать оригинальную функцию с дополнительным уровнем функциональности. Исходная функция должна храниться в закрывающей ячейке одной из функций вложенной структуры декоратора.
Пример:
>>> def add(f):
... def _decorator(*args, **kargs):
... print('hello_world')
... return f(*args, **kargs)
... return _decorator
...
>>> @add
... def f(msg):
... print('f ==>', msg)
...
>>> f('alice')
hello_world
f ==> alice
>>> f.__closure__[0].cell_contents
<function f at 0x7f5d205991e0>
>>> f.__closure__[0].cell_contents('alice')
f ==> alice
это основной принцип недекорации, вы можете обратиться к исходному коду для более подробной информации.
Обычный подход к тестированию таких функций - сделать любые зависимости, такие как get_connection, настраиваемыми. Затем вы можете переопределить его с помощью макета во время тестирования. По сути, это то же самое, что внедрение зависимостей в мире Java, но намного проще благодаря динамической природе Pythons.
Код для этого может выглядеть примерно так:
# decorator definition
def with_connection(f):
def decorated(*args, **kwargs):
f(with_connection.connection_getter(), *args, **kwargs)
return decorated
# normal configuration
with_connection.connection_getter = lambda: get_connection(...)
# inside testsuite setup override it
with_connection.connection_getter = lambda: "a mock connection"
В зависимости от вашего кода, вы можете найти лучший объект, чем декоратор, чтобы использовать заводскую функцию. Проблема с наличием его в декораторе заключается в том, что вам нужно помнить, чтобы восстановить его до старого значения в методе teardown.
Добавьте декоратор, который ничего не делает:
def do_nothing(f):
return f
После определения или импорта with_connection, но перед тем, как перейти к методам, которые используют его в качестве декоратора, добавьте:
if TESTING:
with_connection = do_nothing
Затем, если вы установите для глобального TESTING значение True, вы замените with_connection декоратором бездействия.