如何从 Python 中的函数中去除装饰器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1166118/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 21:37:39  来源:igfitidea点击:

How to strip decorators from a function in Python

pythondecorator

提问by Herge

Let's say I have the following:

假设我有以下内容:

def with_connection(f):
    def decorated(*args, **kwargs):
        f(get_connection(...), *args, **kwargs)
    return decorated

@with_connection
def spam(connection):
    # Do something

I want to test the spamfunction without going through the hassle of setting up a connection (or whatever the decorator is doing).

我想测试该spam功能,而无需经历设置连接的麻烦(或装饰器正在做的任何事情)。

Given spam, how do I strip the decorator from it and get the underlying "undecorated" function?

鉴于spam,我如何从中剥离装饰器并获得底层的“未装饰”功能?

采纳答案by balpha

In the general case, you can't, because

在一般情况下,你不能,因为

@with_connection
def spam(connection):
    # Do something

is equivalent to

相当于

def spam(connection):
    # Do something

spam = with_connection(spam)

which means that the "original" spam might not even exist anymore. A (not too pretty) hack would be this:

这意味着“原始”垃圾邮件甚至可能不再存在。一个(不太漂亮的)hack 是这样的:

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

回答by Alex Volkov

There's been a bit of an update for this question. If you're using Python 3, you can use __wrapped__property for decorators from stdlib.

这个问题有一些更新。如果您使用的是 Python 3,则可以使用__wrapped__来自 stdlib 的装饰器的属性。

Here's an example from Python Cookbook, 3rd edition, section 9.3 Unwrapping decorators

这是Python Cookbook,第 3 版,第 9.3 节解包装饰器中的一个示例

>>> @somedecorator
>>> def add(x, y):
...     return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>

If you are trying to unwrap a function from custom decorator, the decorator function needs to use wrapsfunction from functoolsSee discussion in Python Cookbook, 3rd edition, section 9.2 Preserving function metadata when writing decorators

如果你正试图从定制的装饰解开的功能,装饰功能需要使用wraps从函数functools中看到的讨论Python的食谱,第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

回答by jcdyer

balpha's solution can be made more generalizable with this meta-decorator:

使用这个元装饰器可以使 balpha 的解决方案更具通用性:

def include_original(dec):
    def meta_decorator(f):
        decorated = dec(f)
        decorated._original = f
        return decorated
    return meta_decorator

Then you can decorate your decorators with @include_original, and every one will have a testable (undecorated) version tucked away inside it.

然后你可以用@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

回答by Wojciech Bederski

Behold, FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

看哪,FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:

 orig_spam = spam.func_closure[0].cell_contents

Edit: For functions/methods decorated more than once and with more complicated decorators you can try using the following code. It relies on the fact, that decorated functions are __name__d differently than the original function.

编辑:对于不止一次装饰的函数/方法以及更复杂的装饰器,您可以尝试使用以下代码。它依赖于这样一个事实,即装饰函数的 __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>

It's not fool proof though. It will fail if the name of the function returned from a decorator is the same as the decorated one. The order of hasattr() checks is also a heuristic, there are decoration chains that return wrong results in any case.

但这不是傻瓜证明。如果从装饰器返回的函数名称与被装饰的函数名称相同,它将失败。hasattr() 检查的顺序也是一种启发式,在任何情况下都有返回错误结果的装饰链。

回答by Oin

You can now use the undecoratedpackage:

您现在可以使用未修饰的包:

>>> from undecorated import undecorated
>>> undecorated(spam)

It goes through the hassle of digging through all the layers of different decorators until it reaches the bottom function and doesn't require changing the original decorators. It works on both Python 2 and Python 3.

它经历了挖掘不同装饰器的所有层,直到到达底部功能的麻烦,并且不需要更改原始装饰器。它适用于 Python 2 和 Python 3。

回答by dbr

Instead of doing...

而不是做...

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)

You could just do...

你可以这样做...

def with_connection(f):
    ...

def spam_f(connection):
    ...

spam = with_connection(spam_f)

...which is all the @decoratorsyntax does - you can then obviously access the original spam_fnormally.

...这是所有@decorator语法 - 然后您显然可以spam_f正常访问原始内容。

回答by Aran-Fey

It's good practice to decorate decorators with functools.wrapslike so:

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

As of Python 3.2, this will automatically add a __wrapped__attribute that lets you retrieve the original, undecorated function:

从 Python 3.2 开始,这将自动添加一个__wrapped__属性,让您检索原始的、未修饰的函数:

>>> spam.__wrapped__
<function spam at 0x7fe4e6dfc048>

However, instead of manually accessing the __wrapped__attribute, it's better to use inspect.unwrap:

但是,__wrapped__与其手动访问属性,不如使用inspect.unwrap

>>> inspect.unwrap(spam)
<function spam at 0x7fe4e6dfc048>

回答by lyu.l

the original function is stored in spam.__closure__[0].cell_contents.
Decorator uses closure to bind original function with extra layer of functionality. The original function must be stored in a closure cell kept by one of the functions in the nested structure of decorator.
Example:

原始函数存储在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

this is the core principle of undecorated, you could refer to the source code for more details.

这是undecorated的核心原理,更多细节可以参考源码。

回答by Ants Aasma

The usual approach to testing such functions is to make any dependencies, such as get_connection, configurable. Then you can override it with a mock while testing. Basically the same as dependency injection in the Java world but a lot simpler thanks to Pythons dynamic nature.

测试此类函数的常用方法是使任何依赖项(例如 get_connection)可配置。然后你可以在测试时用模拟覆盖它。与 Java 世界中的依赖注入基本相同,但由于 Python 的动态特性,它要简单得多。

Code for it might look something like this:

它的代码可能如下所示:

# 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"

Depending on your code you could find a better object than the decorator to stick the factory function on. The issue with having it on the decorator is that you'd have to remember to restore it to the old value in the teardown method.

根据您的代码,您可以找到比装饰器更好的对象来粘贴工厂函数。将它放在装饰器上的问题是您必须记住在拆卸方法中将其恢复为旧值。

回答by PaulMcG

Add a do-nothing decorator:

添加一个什么都不做的装饰器:

def do_nothing(f):
    return f

After defining or importing with_connection but before you get to the methods that use it as a decorator, add:

在定义或导入 with_connection 之后,但在使用它作为装饰器的方法之前,添加:

if TESTING:
    with_connection = do_nothing

Then if you set the global TESTING to True, you will have replaced with_connection with a do-nothing decorator.

然后,如果您将全局 TESTING 设置为 True,您将用一个什么都不做的装饰器替换 with_connection。