Как создать декоратор класса Python, который может обернуть экземпляр, класс и статические методы? - PullRequest
7 голосов
/ 18 ноября 2011

Я хотел бы создать декоратор класса Python (*), который бы мог беспрепятственно обернуть все типы методов, которые может иметь класс: экземпляр, класс и статические.

Это код, который у меня есть на данный момент, с комментирующими его частями:

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)

У меня две проблемы, пытаясь заставить эту работу:

  • При переборе всех вызываемых объектов, как мне узнать, относится ли он к экземпляру, классу или статическому типу?
  • Как перезаписать метод правильной упакованной версией, которая вызывается правильно для каждого из этих случаев?

В настоящее время этот код генерирует различные TypeError s в зависимости от того, что закомментированный фрагмент не закомментирован, например:

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

(*): та же проблема гораздо проще, если вы непосредственно декорируете методы .

Ответы [ 2 ]

4 голосов
/ 18 ноября 2011

Поскольку методы являются обертками для функций, чтобы применить декоратор к методу класса после его создания, необходимо:

  1. Извлеките основную функцию из метода, используя ее атрибут im_func.
  2. Украсьте функцию.
  3. Повторно примените обертку.
  4. Перезаписать атрибут с обернутой, оформленной функцией.

Трудно отличить classmethod от обычного метода после применения декоратора @classmethod; оба вида методов относятся к типу instancemethod. Однако вы можете проверить атрибут im_self и посмотреть, является ли он None. Если это так, это обычный метод экземпляра; в противном случае это classmethod.

Статические методы - это простые функции (декоратор @staticmethod просто предотвращает применение обычного метода-оболочки). Так что вам не нужно делать ничего особенного, похоже, что.

Таким образом, ваш алгоритм выглядит следующим образом:

  1. Получить атрибут.
  2. Это вызывается? Если нет, переходите к следующему атрибуту.
  3. Это его тип types.MethodType? Если это так, то это либо метод класса, либо метод экземпляра.
    • Если im_self равно None, это метод экземпляра. Извлеките базовую функцию с помощью атрибута im_func, украсьте ее и повторно примените метод экземпляра: meth = types.MethodType(func, None, cls)
    • Если im_self не равно None, это метод класса. Извлеките основную функцию через im_func и украсьте ее. Теперь вам нужно повторно применить classmethod декоратор, но вы не можете, потому что classmethod() не принимает класс, поэтому нет способа указать, к какому классу он будет присоединен. Вместо этого вы должны использовать метод декоратора экземпляра: meth = types.MethodType(func, cls, type). Обратите внимание, что type здесь является фактическим встроенным, type.
  4. Если его тип не types.MethodType, то это статический метод или другой необязательный метод, поэтому просто украсьте его.
  5. Установить новый атрибут обратно в класс.

Это несколько изменилось в Python 3 - несвязанные методы - это функции, IIRC. В любом случае это, вероятно, нужно будет полностью переосмыслить там.

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

Существует недокументированная функция, inspect.classify_class_attrs, которая может сказать вам, какие атрибуты являются методами класса или статическими методами. Под капотом он использует isinstance(obj, staticmethod) и isinstance(obj, classmethod) для классификации статических и классовых методов. Следуя этой схеме, это работает как в Python2, так и в 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...