如何创建一个可以包装实例、类和静态方法的Python类装饰器?

7 投票
2 回答
2031 浏览
提问于 2025-04-17 06:36

我想创建一个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错误,比如:

  • 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

有一个没有文档说明的函数,叫做 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)
4

因为方法其实是函数的包装器,所以如果你想在类构造完成后给类的方法加装饰器,你需要:

  1. 通过方法的 im_func 属性提取出底层的函数。
  2. 给这个函数加装饰器。
  3. 重新应用这个包装。
  4. 用加了装饰器的函数覆盖原来的属性。

一旦使用了 @classmethod 装饰器,就很难区分 classmethod 和普通方法;这两种方法都是 instancemethod 类型。不过,你可以检查 im_self 属性,看看它是否是 None。如果是,那就是普通实例方法;如果不是,那就是 classmethod

静态方法其实就是普通函数(@staticmethod 装饰器只是防止了通常的方法包装被应用)。所以对于这些方法,你似乎不需要做什么特别的处理。

所以基本上你的算法看起来是这样的:

  1. 获取属性。
  2. 它是可调用的吗?如果不是,就继续检查下一个属性。
  3. 它的类型是 types.MethodType 吗?如果是,那它要么是类方法,要么是实例方法。
    • 如果它的 im_selfNone,那就是实例方法。通过 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 中,这些内容会有所变化——如果我没记错的话,未绑定的方法在那儿是函数。无论如何,这里可能需要完全重新考虑。

撰写回答