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

如何用Python编写有效的类装饰器?,python,class,scope,closures,decorator,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()" %

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

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
正在查找文件。。。完成。
正在导入测试模块。。。完成。
测试方法1
测试方法2
~~~~~~~~~~~~~~~~~~~~~~~~~~
启动对MyTestClass.TestMethod2()的调试支持
测试方法2
启动对MyTestClass.TestMethod2()的调试支持
测试方法2
----------------------------------------------------------------------
在0.004s中运行1个测试
好啊
您可以发现“TestMethod2”打印了两次

有什么问题吗?我对python中的装饰器的理解正确吗

有什么解决办法吗? 顺便说一句,我不想向类中的每个方法都添加decorator。

考虑这个循环:

for name, func in inspect.getmembers(targetCls, inspect.ismethod):
        def wrapper(*args, **kwargs):
            print ("Start debug support for %s.%s()" % (targetCls.__name__, name))
包装器
最终被调用时,它会查找
名称
的值。在locals()中找不到它,它会在循环的扩展范围中查找(并找到)。但是到那时,循环的
已经结束,
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
在评论中,埃里克森提出了一个更好的解决方案:

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)
的每次调用都会创建自己的函数范围,其中
name
func
的不同值绑定正确;显然,该类正在被修饰,而不仅仅是引发异常,您还可以获得打算添加到该类中的代码。所以很明显,你需要在你的装饰器中寻找一个bug,而不是你是否正在设法编写一个有效的装饰器

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


在这种情况下,我要做的是制作一个方法级的decorator,但由于您不想显式地修饰每个方法,因此您需要制作一个类decorator,它遍历所有方法并将它们传递给方法decorator。这是因为您没有通过闭包让包装器访问您的循环变量;而是将循环变量引用的对象的引用传递给函数(构造并返回包装器的decorator函数);完成后,在下一次迭代中重新绑定循环变量不会影响包装器函数。

这是一个非常常见的问题。您认为
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

谢谢,@eryksun。我认为这样更好。