2011-11-11 10 views
6

Właśnie napisałem klasę dekorator jak poniżej, starał się dodać obsługę debugowania dla każdej metody w klasie docelowej:Jak napisać prawidłowy dekorator klas w języku Python?

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() 

Run powyższym kodzie, wynik jest:

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 

może się okazać, że "TestMethod2" wydrukowano dwukrotnie.

Czy jest problem? Czy moje zrozumienie jest właściwe dla dekoratora w pytonie?

Czy istnieje sposób obejścia tego problemu? BTW, nie chcę dodawać dekoratora do każdej metody w klasie.

Odpowiedz

3

Rozważmy tę pętlę:

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

Kiedy wrapper ostatecznie nazywa, wygląda się wartość name. Nie znajduje go w locals(), szuka go (i znajduje go) w rozszerzonym zakresie for-loop. Ale do tego czasu for-loop zakończył się, a name odnosi się do ostatniej wartości w pętli, tj. TestMethod2.

Więc za każdym razem, gdy wywoływarka jest wywoływana, name ocenia na TestMethod2.

Rozwiązaniem jest utworzenie rozszerzonego zakresu, w którym name jest powiązany z odpowiednią wartością. Można to zrobić za pomocą funkcji, closure, z domyślnymi wartościami argumentów. Domyślne wartości argumentów są oceniane i poprawione w czasie definiowania i przypisane do zmiennych o tej samej nazwie.

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 

W eryksun komentarze sugerują nawet lepsze rozwiązanie:

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 

Teraz closure ma być analizowany tylko raz. Każde wywołanie do closure(name,func) tworzy swój własny zakres funkcji z różnymi wartościami dla name i func poprawnie związanymi.

+0

Pozdrawiamy, @eryksun. Myślę, że to jest lepsze. – unutbu

0

Problem polega na tym, że nie pisze się właściwego dekoratora klasy jako takiego; klasa jest oczywiście dekorowana i nie tylko podnosi wyjątki, dostajesz kod, który chcesz dodać do klasy. Więc wyraźnie musisz szukać błędu w swoim dekoratorze, a nie pytać, czy udaje Ci się napisać poprawny dekorator.

W tym przypadku problem dotyczy zamknięć. W swoim dekoratorze Debug masz pętlę ponad name i func, a dla każdej iteracji pętli definiujesz funkcję wrapper, która jest zamknięciem, które ma dostęp do zmiennych pętli. Problem polega na tym, że po rozpoczęciu następnej iteracji pętli zmieniły się rzeczy, do których odnoszą się zmienne pętli. Ale wywołujesz tylko jedną z tych funkcji opakowania po wykonaniu całej pętli. Tak więc każda udekorowana metoda kończy się wywoływaniem wartości z pętli: w tym przypadku TestMethod2.

W tym przypadku zrobię dekorator na poziomie metody, ale ponieważ nie chcesz jawnie dekorować każdej metody, musisz stworzyć dekorator klasy, który przejdzie przez wszystkie metody i przekaże je dekoratorowi metod. . Działa to, ponieważ nie dajesz otoki dostęp do zmiennej pętli przez zamknięcie; zamiast tego podajesz odniesienie do rzeczy, do której odnosi się zmienna pętli w funkcji (funkcja dekoratora, która konstruuje i zwraca wrappera); gdy to zrobi, nie wpłynie to na funkcję opakowania w celu ponownego powiązania zmiennej pętli w następnej iteracji.

0

Jest to bardzo częsty problem. Myślisz, że wrapper jest zamknięciem, które przechwytuje bieżący argument func, ale tak nie jest. Jeśli nie przekazujesz aktualnej wartości func do opakowania, jego wartość jest podnoszona tylko po pętli, więc otrzymujesz ostatnią wartość.

Można to zrobić:

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 
Powiązane problemy