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

假设我有以下几点:

def with_connection(f): def decorated(*args, **kwargs): f(get_connection(...), *args, **kwargs) return decorated @with_connection def spam(connection): # Do something 

我想testingspamfunction,而不用经历设置连接的麻烦(或者装饰者正在做的事情)。

鉴于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。