如何在Python中编写有效的类装饰器?

时间:2011-11-11 09:27:16

标签: python class scope closures decorator

我刚刚编写了一个类装饰器,如下所示,试图为目标类中的每个方法添加调试支持:

import unittest
import inspect

def Debug(targetCls):
   for name, func in inspect.getmembers(targetCls, inspect.ismethod):
      def wrapper(*args, **kwargs):
         print ("Start debug support for %s.%s()" % (targetCls.__name__, name));
         result = func(*args, **kwargs)
         return result
      setattr(targetCls, name, wrapper)
   return targetCls

@Debug
class MyTestClass:
   def TestMethod1(self):
      print 'TestMethod1'

   def TestMethod2(self):
      print 'TestMethod2'

class Test(unittest.TestCase):

   def testName(self):
      for name, func in inspect.getmembers(MyTestClass, inspect.ismethod):
         print name, func

      print '~~~~~~~~~~~~~~~~~~~~~~~~~~'
      testCls = MyTestClass()

      testCls.TestMethod1()
      testCls.TestMethod2()


if __name__ == "__main__":
   #import sys;sys.argv = ['', 'Test.testName']
   unittest.main()

运行上面的代码,结果是:

Finding files... done.
Importing test modules ... done.

TestMethod1 <unbound method MyTestClass.wrapper>
TestMethod2 <unbound method MyTestClass.wrapper>
~~~~~~~~~~~~~~~~~~~~~~~~~~
Start debug support for MyTestClass.TestMethod2()
TestMethod2
Start debug support for MyTestClass.TestMethod2()
TestMethod2
----------------------------------------------------------------------
Ran 1 test in 0.004s

OK

您可以找到“TestMethod2”打印两次。

有问题吗?我的理解是否适合python中的装饰器?

有没有解决方法? 顺便说一句,我不想​​为这个类中的每个方法添加装饰器。

3 个答案:

答案 0 :(得分:3)

考虑这个循环:

for name, func in inspect.getmembers(targetCls, inspect.ismethod):
        def wrapper(*args, **kwargs):
            print ("Start debug support for %s.%s()" % (targetCls.__name__, name))

最终调用wrapper时,会查找name的值。没有在locals()中找到它,它在for-loop的扩展范围内查找(并找到它)。但到那时for-loop已经结束,而name指的是循环中的最后一个值,即TestMethod2

因此,无论何时调用包装器,name都会计算为TestMethod2

解决方案是创建一个扩展范围,其中name绑定到正确的值。这可以使用函数closure和默认参数值来完成。默认参数值在定义时计算并固定,并绑定到同名变量。

def Debug(targetCls):
    for name, func in inspect.getmembers(targetCls, inspect.ismethod):
        def closure(name=name,func=func):
            def wrapper(*args, **kwargs):
                print ("Start debug support for %s.%s()" % (targetCls.__name__, name))
                result = func(*args, **kwargs)
                return result
            return wrapper        
        setattr(targetCls, name, closure())
    return targetCls

在评论中,eryksun提出了一个更好的解决方案:

def Debug(targetCls):
    def closure(name,func):
        def wrapper(*args, **kwargs):
            print ("Start debug support for %s.%s()" % (targetCls.__name__, name));
            result = func(*args, **kwargs)
            return result
        return wrapper        
    for name, func in inspect.getmembers(targetCls, inspect.ismethod):
        setattr(targetCls, name, closure(name,func))
    return targetCls

现在closure只需要解析一次。每次调用closure(name,func)都会创建自己的函数范围,并且namefunc的明确值会正确绑定。

答案 1 :(得分:0)

问题不在于编写有效的类装饰器;这个类显然正在被装饰,并且不仅仅是引发异常,而是你想要添加到类中的代码。很明显,你需要在你的装饰师中寻找一个错误,而不是你是否有能力写一个有效的装饰师的问题。

在这种情况下,问题在于闭包。在Debug装饰器中,循环遍历namefunc,并为每个循环迭代定义一个函数wrapper,这是一个可以访问的闭包循环变量。问题是,一旦下一个循环迭代开始,循环变量引用的内容就会发生变化。但是,只有在完成整个循环之后才调用任何这些包装函数。因此,每个修饰的方法最终都会调用循环中的 last 值:在这种情况下,TestMethod2

在这种情况下我要做的是创建一个方法级别的装饰器,但是因为你不想显式地装饰每个方法,然后你创建一个类装饰器,它遍历所有方法并将它们传递给方法装饰器。这是有效的,因为你没有通过闭包给包装器访问你的循环变量;你改为将循环变量所引用的东西的引用传递给一个函数(构造并返回一个包装器的装饰器函数);一旦完成,它不会影响包装函数在下一次迭代时重新绑定循环变量。

答案 2 :(得分:0)

这是一个非常常见的问题。您认为wrapper是一个捕获当前func参数的闭包,但事实并非如此。如果你没有将当前的func值传递给包装器,那么它的值只会在循环之后被查找,所以你得到最后一个值。

你可以这样做:

def Debug(targetCls):

   def wrap(name,func): # use the current func
      def wrapper(*args, **kwargs):
         print ("Start debug support for %s.%s()" % (targetCls.__name__, name));
         result = func(*args, **kwargs)
         return result
      return wrapper

   for name, func in inspect.getmembers(targetCls, inspect.ismethod):
      setattr(targetCls, name, wrap(name, func))
   return targetCls