类装饰器、继承、super()与最大递归

7 投票
5 回答
6984 浏览
提问于 2025-04-15 21:01

我正在尝试弄明白如何在使用 super() 的子类上使用装饰器。因为我的类装饰器会创建另一个子类,所以当它改变传给 super(className, self)className 时,似乎会导致装饰过的类无法使用 super()。下面是一个例子:

def class_decorator(cls):
    class _DecoratedClass(cls):
        def __init__(self):
            return super(_DecoratedClass, self).__init__()
    return _DecoratedClass

class BaseClass(object):
    def __init__(self):
        print "class: %s" % self.__class__.__name__
    def print_class(self):
        print "class: %s" % self.__class__.__name__

bc = BaseClass().print_class()

class SubClass(BaseClass):
    def print_class(self):
        super(SubClass, self).print_class()

sc = SubClass().print_class()

@class_decorator
class SubClassAgain(BaseClass):
    def print_class(self):
        super(SubClassAgain, self).print_class()

sca = SubClassAgain()
# sca.print_class() # Uncomment for maximum recursion

输出应该是:

class: BaseClass
class: BaseClass
class: SubClass
class: SubClass
class: _DecoratedClass
Traceback (most recent call last):
File "class_decorator_super.py", line 34, in <module>
sca.print_class()
File "class_decorator_super.py", line 31, in print_class
super(SubClassAgain, self).print_class()
...
...
RuntimeError: maximum recursion depth exceeded while calling a Python object

有没有人知道在使用装饰器时,如何不破坏使用 super() 的子类?理想情况下,我希望能不时重用一个类,并简单地给它加上装饰,而不影响它的功能。

5 个回答

3

你可能已经知道,问题出在名字 SubClassAgainSubClassAgain.print_class 中是局限于当前模块的全局命名空间的。因此,SubClassAgain 实际上指的是类 _DecoratedClass,而不是被装饰的那个类。要获取被装饰的类,可以遵循一个约定,就是类装饰器有一个属性指向被装饰的类。

def class_decorator(cls):
    class _DecoratedClass(cls):
        original=cls
        def __init__(self):
            print '_DecoratedClass.__init__'
            return super(_DecoratedClass, self).__init__()
    return _DecoratedClass

@class_decorator
class SubClassAgain(BaseClass):
    original
    def print_class(self):
        super(self.__class__.original, self).print_class()

另一种方法是使用 __bases__ 属性来获取被装饰的类。

@class_decorator
class SubClassAgain(BaseClass):
    def print_class(self):
        super(self.__class__.__bases__[0], self).print_class()

当然,如果有多个装饰器,这两种方法都变得很麻烦。而且后者在处理被装饰类的子类时也不管用。你可以结合装饰器和混入(mixin),写一个装饰器把混入添加到一个类中。不过,这样做并不能帮助你重写方法。

def class_decorator(cls):
    class _DecoratedClass(object):
        def foo(self):
            return 'foo'
    cls.__bases__ += (_DecoratedClass, )
    return cls

最后,你可以直接操作类的属性来设置方法。

def class_decorator(cls):
    old_init = getattr(cls, '__init__')
    def __init__(self, *args, **kwargs):
        print 'decorated __init__'
        old_init(self, *args, **kwargs)
    setattr(cls, '__init__', __init__)
    return cls

对于你的例子来说,这可能是最好的选择,尽管基于混入的装饰器也有它的用处。

5

基本上,你可以在交互式Python提示符下输入你的代码示例后看到问题:

>>> SubClassAgain
<class '__main__._DecoratedClass'>

也就是说,名字 SubClassAgain 现在被绑定(在全局范围内,在这种情况下)到一个类,这个类实际上不是“真正的” SubClassAgain,而是它的一个子类。所以,任何对这个名字的晚绑定引用,比如你在 super(SubClassAgain, 调用中的那个,当然会得到那个假装成这个名字的子类——那个子类的父类当然是“真正的 SubClassAgain”,因此导致了无限递归。

你可以非常简单地重现同样的问题,只需让任何子类夺取其基类的名字:

>>> class Base(object):
...   def pcl(self): print 'cl: %s' % self.__class__.__name__
... 
>>> class Sub(Base):
...   def pcl(self): super(Sub, self).pcl()
... 
>>> Sub().pcl()
cl: Sub
>>> class Sub(Sub): pass
... 

现在, Sub().pcl() 将导致无限递归,因为“名字夺取”。类装饰,除非你用它来装饰并返回你作为参数得到的同一个类,否则就是系统性的“名字夺取”,因此与那些必须返回该名字的“真实”类的用法不兼容,而不是夺取者(无论是在 self 中还是其他地方)。

解决方法——如果你绝对需要同时有类装饰作为夺取(不仅仅是通过改变接收到的类参数进行的类装饰),并且 super——基本上需要一些协议来协调夺取者和可能被夺取者之间的合作,比如对你的示例代码进行以下小改动:

def class_decorator(cls):
    class _DecoratedClass(cls):
    _thesuper = cls
        def __init__(self):
            return super(_DecoratedClass, self).__init__()
    return _DecoratedClass

   ...

@class_decorator
class SubClassAgain(BaseClass):
    def print_class(self):
    cls = SubClassAgain
    if '_thesuper' in cls.__dict__:
        cls = cls._thesuper
        super(cls, self).print_class()
3

这个装饰器会导致一种类似于“钻石继承”的问题。为了避免这些麻烦,你可以不使用 super()。把 SubClassAgain 改成下面这样,就可以防止无限递归的问题:

@class_decorator
class SubClassAgain(BaseClass):
    def print_class(self):
        BaseClass.print_class(self)

撰写回答