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

Posted

技术标签:

【中文标题】如何在 Python 中编写有效的类装饰器?【英文标题】:How to Write a valid Class Decorator in Python? 【发布时间】:2011-12-26 21:02:23 【问题描述】:

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

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 中的装饰器的理解是否正确?

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

【问题讨论】:

【参考方案1】:

考虑这个循环:

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

在 cmets 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 的不同值绑定正确。

【讨论】:

谢谢,@eryksun。我认为这样更好。【参考方案2】:

问题不在于这样编写有效的类装饰器;该类显然正在被修饰,并且不仅引发异常,您还可以编写要添加到该类中的代码。所以很明显,你需要在你的装饰器中寻找一个错误,而不是你是否正在设法编写一个有效的装饰器。

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

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

【讨论】:

【参考方案3】:

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

【讨论】:

以上是关于如何在 Python 中编写有效的类装饰器?的主要内容,如果未能解决你的问题,请参考以下文章

Python 联合 - 在另一个装饰器中收集多个 @patch 装饰器

如何让 sphinx 识别装饰的 python 函数

如何让我的类装饰器只在继承链中的最外层类上运行?

其他类方法的类装饰器[重复]

python装饰器

Python:异常装饰器。如何保留堆栈跟踪