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

Posted

技术标签:

【中文标题】如何从 Python 中的函数中去除装饰器【英文标题】:How to strip decorators from a function in Python 【发布时间】:2010-11-13 01:26:50 【问题描述】:

假设我有以下内容:

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

@with_connection
def spam(connection):
    # Do something

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

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

【问题讨论】:

【参考方案1】:

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

@with_connection
def spam(connection):
    # Do something

等价于

def spam(connection):
    # Do something

spam = with_connection(spam)

这意味着“原始”垃圾邮件甚至可能不再存在。一个(不太漂亮的)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

【讨论】:

如果你要修改代码调用_original你不妨把装饰器注释掉。 这对于 Python 3 来说已经过时了。请参阅下面的答案:***.com/a/33024739/13969【参考方案2】:

看哪,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() 检查的顺序也是启发式的,有装饰链在任何情况下都会返回错误的结果。

【讨论】:

func_closure 在 3.x 中被 __closure__ 取代,并且已经在 2.6 中 我在玩函数时看到了这一点,但如果你在一个函数上使用多个装饰器,它就会变得复杂。你最终会打电话给.func_closure[0].cell_contents,直到cell_contents is None。我希望有一个更优雅的解决方案。 如果装饰器使用 functools.wraps 可能不起作用 想出了相同的解决方案,赞 ^^ @EvgeniiPuchkaryov 它似乎与 functools.wrap 一起使用【参考方案3】:

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

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

【讨论】:

有没有办法扩展它,以便在最外层的装饰函数中可以访问最深层的原件,所以我不必为包装在三个装饰器中的函数做 ._original._original._original ? @jcdyer 装饰你的装饰器到底是什么意思?我可以做一些类似 \@include_original (next line) \@decorator_which_I_dont_control (next line) function_definition 的事情吗? @Harshdeep:你会想做类似now_i_control = include_original(decorator_i_dont_control) 的事情,然后用@now_i_control\ndef function(): 装饰你的函数。请注意,y = foo(y) 在语法上等同于@foo\ndef y():。如果你尝试了你的建议,你最终会得到include_original(decorator_i_dont_control(function)),而你想要的是include_original(decorator_i_dont_control)(function) @Harshdeep 我刚刚用示例用法编辑了我的回复。同样,如果你没有自己定义装饰器,你可以用decorator = include_original(decorator) 包裹它 @jcdyer 谢谢 :) .. 我也找到了类似的解决方案 def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decorated【参考方案4】:

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

它的代码可能如下所示:

# 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 方法中将其恢复为旧值。

【讨论】:

【参考方案5】:

而不是做...

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

【讨论】:

不错的方法,太聪明了! 使用装饰器的重点在于简单地重用它。如果我在需要装饰器的地方都有代码,这不会扩展。【参考方案6】:

添加一个无所事事的装饰器:

def do_nothing(f):
    return f

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

if TESTING:
    with_connection = do_nothing

然后,如果您将全局 TESTING 设置为 True,您将用一个无所事事的装饰器替换 with_connection。

【讨论】:

【参考方案7】:

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

这是来自Python Cookbook, 3rd edition, section 9.3 Unwrapping decorators的示例

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

如果您尝试从自定义装饰器中解包函数,则装饰器函数需要使用来自functoolswraps 函数请参阅Python Cookbook, 3rd edition, section 9.2 Preserving function metadata when writing decorators 中的讨论

>>> from functools import wraps
>>> def somedecorator(func):
...    @wraps(func)
...    def wrapper(*args, **kwargs):
...       # decorator implementation here
...       # ......
...       return func(*args, **kwargs)
...
...    return wrapper

【讨论】:

Python3 的胜利! 这是不真实的。如果使用functools.wraps 装饰,则装饰函数只有__wrapped__ 属性。此外,链接已失效。 我在实现自己的装饰器时修复了本书的链接并扩展了案例的答案。 这对我有用。附加信息:如果您的函数有多个装饰器,您可以链接多个 .__wrapped__ 以访问原始函数。【参考方案8】:

您现在可以使用undecorated 包:

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

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

【讨论】:

这正是我一直在寻找的! 史诗!也需要这个。【参考方案9】:

最好用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>

【讨论】:

【参考方案10】:

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

这是undecorated的核心原理,具体可以参考源码。

【讨论】:

以上是关于如何从 Python 中的函数中去除装饰器的主要内容,如果未能解决你的问题,请参考以下文章

理解Python装饰器

一文带你了解 Python 中的装饰器

Python 装饰器装饰类中的方法

Python中的装饰器之写一个装饰器

如何将装饰器中的变量传递给装饰函数中的函数参数?

如何理解Python中的装饰器?