2011-11-18 18 views
7

Chciałbym stworzyć dekorator klas Python (*), który byłby w stanie płynnie zawrzeć wszystkie typy metod, jakie klasa może mieć: instancja, klasa i statyczne.Jak utworzyć dekorator klas Pythona, który jest w stanie objąć metody instancji, klasy i statyczne?

Jest to kod mam teraz, z częściami, które rozkładają to skomentował:

def wrapItUp(method): 
    def wrapped(*args, **kwargs): 
     print "This method call was wrapped!" 
     return method(*args, **kwargs) 
    return wrapped 

dundersICareAbout = ["__init__", "__str__", "__repr__"]#, "__new__"] 

def doICareAboutThisOne(cls, methodName): 
    return (callable(getattr(cls, methodName)) 
      and (not (methodName.startswith("__") and methodName.endswith("__")) 
      or methodName in dundersICareAbout)) 

def classDeco(cls): 
    myCallables = ((aname, getattr(cls, aname)) for aname in dir(cls) if doICareAboutThisOne(cls, aname)) 
    for name, call in myCallables: 
     print "*** Decorating: %s.%s(...)" % (cls.__name__, name) 
     setattr(cls, name, wrapItUp(call)) 
    return cls 

@classDeco 
class SomeClass(object): 

    def instanceMethod(self, p): 
     print "instanceMethod: p =", p 

    @classmethod 
    def classMethod(cls, p): 
     print "classMethod: p =", p 

    @staticmethod 
    def staticMethod(p): 
     print "staticMethod: p =", p 


instance = SomeClass() 
instance.instanceMethod(1) 
#SomeClass.classMethod(2) 
#instance.classMethod(2) 
#SomeClass.staticMethod(3) 
#instance.staticMethod(3) 

Mam dwie kwestie próbuje uczynić tę pracę:

  • podczas iteracji nad wszystkim kalki, jak się dowiedzieć, czy jest to instancja, klasa czy typ statyczny?
  • Jak zastąpić metodę odpowiednią owiniętą wersją, która jest poprawnie wywoływana dla każdego z tych przypadków?

Obecnie, ten kod generuje różne TypeError sw zależności od tego, co skomentował fragment jest komentarzem, jak:

  • TypeError: unbound method wrapped() must be called with SomeClass instance as first argument (got int instance instead)
  • TypeError: classMethod() takes exactly 2 arguments (3 given)

(*): To samo problem jest znacznie prostszy, jeśli jesteś decorating the methods directly.

+0

Fajnie będzie zobaczyć, jak różni deweloperzy radzą sobie z tym samym, skomplikowanym problemem. – wberry

+0

@wberry: tak, już czytam bieżące odpowiedzi i zauważyłem, że trudno będzie wybrać "odpowiedni". – Chuim

Odpowiedz

4

Ponieważ metody są owijarki do funkcji, aby zastosować dekorator sposobu na klasę po klasa została skonstruowana, trzeba:

  1. Rozpakuj podstawową funkcję z metodą przy użyciu jego atrybut im_func.
  2. Udekoruj funkcję.
  3. Ponownie załóż opakowanie.
  4. Zastąp atrybut za pomocą zapakowanej, dekorowanej funkcji.

Trudno jest odróżnić classmethod od zwykłej metody po zastosowaniu dekoratora @classmethod; oba rodzaje metod są typu instancemethod. Można jednak sprawdzić atrybut im_self i sprawdzić, czy jest to None. Jeśli tak, to jest to zwykła metoda instancji; w przeciwnym razie jest to classmethod.

Metody statyczne są prostymi funkcjami (dekorator @staticmethod ogranicza się do zablokowania zwykłego opakowania metody). Więc nie musisz robić dla nich nic specjalnego, wygląda na to.

Więc w zasadzie algorytmu wygląda tak:

  1. Uzyskaj atrybut.
  2. Czy można to wywołać? Jeśli nie, przejdź do następnego atrybutu.
  3. Czy jest to typ types.MethodType? Jeśli tak, to jest to metoda klasy lub metoda instancji.
    • Jeśli jego im_self jest None, jest to metoda instancji. Wyodrębnij funkcję bazową za pomocą atrybutu im_func, udekoruj i ponownie zastosuj metodę instancji: meth = types.MethodType(func, None, cls)
    • Jeśli jej im_self nie jest None, jest to metoda klasy. Zanotuj podstawową funkcję poprzez im_func i udekoruj ją. Teraz musisz ponownie zastosować dekorator classmethod, ale nie możesz, ponieważ classmethod() nie bierze klasy, więc nie ma możliwości określenia, do której klasy będzie dołączony. Zamiast tego musisz użyć dekoratora metody instancji: meth = types.MethodType(func, cls, type). Zauważ, że type jest tutaj wbudowany, type.
  4. Jeśli jego typ nie jest types.MethodType, to jest to metoda statyczna lub inna niezwiązana podpowiedź, więc po prostu ją udekoruj.
  5. Ustaw nowy atrybut z powrotem na klasę.

Zmieniają się one nieco w Pythonie 3 - tam są funkcje niezwiązane, IIRC. W każdym razie prawdopodobnie będzie to musiało być całkowicie przemyślane.

+0

Wybierz tę odpowiedź, ponieważ wyraźnie wyjaśnia ona podstawowy problem. Dzięki i przepraszam za długie opóźnienie! – Chuim

3

Istnieje nieudokumentowana funkcja, inspect.classify_class_attrs, która mówi, które atrybuty są metodami klasycznymi lub metodami statycznymi. Pod maską używa isinstance(obj, staticmethod) i isinstance(obj, classmethod) do klasyfikowania metod statycznych i klasowych. Zgodnie z tym wzorcem działa to zarówno w języku Python2, jak i Python3:

def wrapItUp(method,kind='method'): 
    if kind=='static method': 
     @staticmethod 
     def wrapped(*args, **kwargs): 
      return _wrapped(*args,**kwargs) 
    elif kind=='class method': 
     @classmethod 
     def wrapped(cls,*args, **kwargs): 
      return _wrapped(*args,**kwargs)     
    else: 
     def wrapped(self,*args, **kwargs): 
      return _wrapped(self,*args,**kwargs)         
    def _wrapped(*args, **kwargs): 
     print("This method call was wrapped!") 
     return method(*args, **kwargs) 
    return wrapped 
def classDeco(cls): 
    for name in (name 
       for name in dir(cls) 
       if (callable(getattr(cls,name)) 
        and (not (name.startswith('__') and name.endswith('__')) 
          or name in '__init__ __str__ __repr__'.split())) 
       ): 
     method = getattr(cls, name) 
     obj = cls.__dict__[name] if name in cls.__dict__ else method 
     if isinstance(obj, staticmethod): 
      kind = "static method" 
     elif isinstance(obj, classmethod): 
      kind = "class method" 
     else: 
      kind = "method" 
     print("*** Decorating: {t} {c}.{n}".format(
      t=kind,c=cls.__name__,n=name)) 
     setattr(cls, name, wrapItUp(method,kind)) 
    return cls 

@classDeco 
class SomeClass(object): 
    def instanceMethod(self, p): 
     print("instanceMethod: p = {}".format(p)) 
    @classmethod 
    def classMethod(cls, p): 
     print("classMethod: p = {}".format(p)) 
    @staticmethod 
    def staticMethod(p): 
     print("staticMethod: p = {}".format(p)) 

instance = SomeClass() 
instance.instanceMethod(1) 
SomeClass.classMethod(2) 
instance.classMethod(2) 
SomeClass.staticMethod(3) 
instance.staticMethod(3)