Как написать правильный декоратор класса в Python? - PullRequest
6 голосов
/ 11 ноября 2011

Я только что написал декоратор класса, как показано ниже, попытался добавить поддержку отладки для каждого метода в целевом классе:

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 ]

3 голосов
/ 11 ноября 2011

Рассмотрим этот цикл:

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) создает свою собственную область функций с различными значениями для name и func, которые связаны правильно.

0 голосов
/ 11 ноября 2011

Это очень распространенная проблема. Вы думаете, 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
0 голосов
/ 11 ноября 2011

Проблема не в том, чтобы написать правильный декоратор класса как таковой;класс, очевидно, оформлен и не просто вызывает исключения, вы получаете код, который вы намеревались добавить в класс.Очевидно, вам нужно искать ошибку в вашем декораторе, а не вопросы о том, удается ли вам написать корректный декоратор.

В этом случае проблема заключается в замыканиях.В вашем декораторе Debug вы перебираете циклы name и func, и для каждой итерации цикла вы определяете функцию wrapper, которая является закрытием того, что имеет доступ к переменным цикла .Проблема в том, что как только начинается следующая итерация цикла, вещи, на которые ссылаются переменные цикла, изменились.Но вы когда-либо вызываете любую из этих функций-оболочек после , когда весь цикл завершен.Таким образом, каждый декорированный метод в конечном итоге вызывает значения last из цикла: в этом случае TestMethod2.

В этом случае я бы сделал декоратор уровня метода, но так как вы не хотите явно декорировать каждый метод, вы создаете декоратор класса, который проходит через все методы и передает их декоратору метода.Это работает, потому что вы не предоставляете оболочке доступ к вашей переменной цикла через замыкание;вместо этого вы передаете ссылку на объект, на который ссылается переменная цикла, в функцию (функция декоратора, которая создает и возвращает обертку);как только это будет сделано, это не повлияет на функцию-оболочку для повторного связывания переменной цикла на следующей итерации.

...