如何从Python函数中剥离装饰器


68

假设我有以下几点:

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

@with_connection
def spam(connection):
    # Do something

我想测试该spam功能而无需经历建立连接的麻烦(或装饰者所做的任何事情)。

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

Answers:


48

在一般情况下,您不能这样做,因为

@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

1
如果您要修改代码以进行调用_original,则最好将装饰器注释掉。
— eduffy

57

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

这是第3版Python Cookbook中的示例,第9.3节展开装饰器

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

如果你正试图从定制的装饰解开的功能,装饰功能需要使用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

5
赢得Python3!
— 放克

1
这是不正确的。如果用修饰装饰的函数,则只有一个__wrapped__属性functools.wraps。此外,链接已死。
— Aran-Fey

我修复了与书的链接,并扩展了实现自己的装饰器时的答案。
— 亚历克斯·沃尔科夫

这对我有用。其他信息:如果您的函数有多个装饰器,则可以将多个装饰器链接起来.__wrapped__以获取原始函数。
— Erik Kalkoken

33

可以使用此元修饰器使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

1
有没有一种方法可以扩展此范围,以便在最外层的装饰函数处可以访问最深层的原件,因此对于包裹在三个装饰器中的函数,我不必执行._original._original._original吗?
— Sparr

@jcdyer装饰您的装饰器的确切含义是什么?我可以做类似\ @include_original(下一行)\ @decorator_which_I_dont_control(下一行)function_definition的操作吗?
— Harshdeep

1
@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)
— jcdyer

1
@Harshdeep我刚刚编辑了示例用法的回复。同样,如果您自己没有定义装饰器,则可以使用decorator = include_original(decorator)
— jcdyer

@jcdyer谢谢:) ..我也到达了类似的解决方案 def include_original(f): @wraps(f) def decorated(*args, **kwargs): return f(*args, **kwargs) decorated._original = f return decorated
— Harshdeep

18

看得出来,FuglyHack可以为您的示例效劳,但ICantPromise还有其他方面:

 orig_spam = spam.func_closure[0].cell_contents

编辑:对于多次装饰的函数/方法,以及使用更复杂的装饰器,您可以尝试使用以下代码。它依赖于这样一个事实,即修饰函数的名称与原始函数的名称不同。

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()检查的顺序也是一种试探法,在任何情况下都有装饰链返回错误的结果。


4
func_closure__closure__在3.x中已被替换,而在2.6中已经被替换
— Ivan Baldin 09年

1
我在使用函数时看到了这一点,但是如果您在一个函数上使用多个装饰器,这会变得很复杂。您结束通话.func_closure[0].cell_contents直到cell_contents is None。我希望有一个更优雅的解决方案。
— 合并

如果装饰器使用functools.wraps
— Evgen

提出了相同的解决方案,荣誉^^ @EvgeniiPuchkaryov似乎可以与functools.wrap一起使用
— Romuald Brunet

10

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

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

它经历了遍历不同装饰器的所有层的麻烦,直到到达底部功能为止,而无需更改原始装饰器。它同时适用于Python 2和Python 3。


这正是我一直在寻找的!
— samaspin '18年

史诗!也需要这个。
— ArielB

6

而不是做...

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正常访问原始文件。


不错的方法,这么聪明!
— laike9m 2016年

使用装饰器的全部目的是简单地重用它。如果我在需要装饰器的所有地方都编写了代码,则无法缩放。
— dmvianna

5

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>

3

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

这是未修饰的核心原则,您可以参考源代码以获取更多详细信息。


2

测试此类功能的常用方法是使任何依赖项(例如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"

根据您的代码,可以找到比装饰器更好的对象来保留工厂功能。将其放在装饰器上的问题是,您必须记住要在拆解方法中将其还原为旧值。


1

添加不做装饰器:

def do_nothing(f):
    return f

在定义或导入with_connection之后,但在进入将其用作装饰器的方法之前,添加:

if TESTING:
    with_connection = do_nothing

然后,如果将全局TESTING设置为True,则将用空连接修饰符替换为with_connection。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.