Как убрать декораторы из функции в 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 декоратором бездействия.

Другие вопросы по тегам