如何从 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
>>>
如果您尝试从自定义装饰器中解包函数,则装饰器函数需要使用来自functools
的wraps
函数请参阅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 中的函数中去除装饰器的主要内容,如果未能解决你的问题,请参考以下文章