如何从python中的函数中去除装饰器
假设我有以下几点:
def with_connection(f): def decorated(*args, **kwargs): f(get_connection(...), *args, **kwargs) return decorated @with_connection def spam(connection): # Do something
我想testingspam
function,而不用经历设置连接的麻烦(或者装饰者正在做的事情)。
鉴于spam
,我如何从它剥离装饰,并获得底层“未修饰”的function?
在一般情况下,你不能,因为
@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
这个meta-decorator可以使得balpha的解决scheme更具概括性:
def include_original(dec): def meta_decorator(f): decorated = dec(f) decorated._original = f return decorated return meta_decorator
然后你可以用@include_original来装饰你的装饰器,每个人都会有一个可testing的(未装饰的)版本。
@include_original def shout(f): def _(): string = f() return string.upper() @shout def function(): return "hello world" >>> print function() HELLO_WORLD >>> print function._original() hello world
看哪,FuglyHackThatWillWorkForYourExampleButICantPromiseAnythingElse:
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>
尽pipe这并不是很好的证据。 如果从装饰器返回的函数的名称与装饰器的名称相同,将会失败。 hasattr()检查的顺序也是启发式的,在任何情况下,装饰链都会返回错误的结果。
这个问题有一些更新。 如果您使用Python 3,则可以使用返回包装函数的__wrapped__
属性。
下面是Python Cookbook第3版的一个例子
>>> @somedecorator >>> def add(x, y): ... return x + y ... >>> orig_add = add.__wrapped__ >>> orig_add(3, 4) 7 >>>
查看关于该属性更详细的用法的讨论。
而不是做..
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
testing这些函数的常用方法是使任何依赖项(如get_connection)可configuration。 然后你可以在testing的时候用模拟来覆盖它。 基本上和Java世界中的dependency injection一样,但是由于Pythons的dynamic特性,这要简单得多。
代码可能看起来像这样:
# 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"
根据你的代码,你可以find比修饰器更好的对象来粘贴工厂function。 把它放在装饰器上的问题是,你必须记得在拆卸方法中将它恢复到旧值。
您现在可以使用未修饰的包:
>>> from undecorated import undecorated >>> undecorated(spam)
它通过挖掘不同的装饰器的所有层的麻烦,直到它到达底层function,并不需要改变原来的装饰器。 适用于python2和python3。
添加一个无所作为的装饰器:
def do_nothing(f): return f
在定义或导入with_connection之后,在您将其作为装饰器使用的方法之前,请添加:
if TESTING: with_connection = do_nothing
那么,如果你把全局TESTING设置为True,你将用一个无操作的装饰器replacewith_connection。