神坑·Python 装饰类无限递归
本文首发于我的博客,转载请注明出处
《神坑》系列将会不定期更新一些可遇而不可求的坑
防止他人入坑,也防止自己再次入坑
简化版问题
现有两个 View
类:
class View(object): def method(self): # Do something... pass class ChildView(View): def method(self): # Do something else ... super(ChildView, self).method()
以及一个用于修饰该类的装饰器函数 register
——用于装饰类的装饰器很常见(如 django.contrib.admin
的 register
),通常可极大地减少定义相似类时的工作量:
class Mixin(object): pass def register(cls): return type( 'DecoratedView', (Mixin, cls), {} )
这个装饰器为被装饰类附加上一个额外的父类 Mixin
,以增添自定义的功能。
完整的代码如下:
class Mixin(object): pass def register(cls): return type( 'DecoratedView', (Mixin, cls), {} ) class View(object): def method(self): # Do something... pass @register class ChildView(View): def method(self): # Do something else ... super(ChildView, self).method()
看上去似乎没什么问题。然而一旦调用 ChildView().method()
,却会报出诡异的 无限递归 错误:
# ... File "test.py", line 23, in method super(ChildView, self).method() File "test.py", line 23, in method super(ChildView, self).method() File "test.py", line 23, in method super(ChildView, self).method() RuntimeError: maximum recursion depth exceeded while calling a Python object
【一脸懵逼】
猜想 & 验证
从 Traceback 中可以发现:是 super(ChildView, self).method()
在不停地调用自己——这着实让我吃了一惊,因为 按理说 super
应该沿着继承链查找父类,可为什么在这里 super
神秘地失效了呢?
为了验证 super(...).method
的指向,可以尝试将该语句改为 print(super(ChildView, self).method)
,并观察结果:
<bound method ChildView.method of <__main__.ChildView object at 0xb70fec6c>>
输出表明: method
的指向确实有误,此处本应为 View.method
。
super
是 python 内置方法,肯定不会出错。那,会不会是 super
的参数有误呢?
super
的签名为 super(cls, instance)
,宏观效果为 遍历 cls
的继承链查找父类方法,并以 instance
作为 self
进行调用。如今查找结果有误,说明 继承链是错误的,因而极有可能是 cls
出错。
因此,有必要探测一下 ChildView
的指向。在 method
中加上一句: print(ChildView)
:
<class '__main__.DecoratedView'>
原来,作用域中的 ChildView
已经被改变了。
真相
一切都源于装饰器语法糖。我们回忆一下装饰器的等价语法:
@decorator class Class: pass
等价于
class Class: pass Class = decorator(Class)
这说明:装饰器会更改该作用域内被装饰名称的指向。
这本来没什么,但和 super
一起使用时却会出问题。通常情况下我们会将本类的名称传给 super
(在这里为 ChildView
),而本类名称和装饰器语法存在于同一作用域中,从而在装饰时被一同修改了(在本例中指向了子类 DecoratedView
),进而使 super(...).method
指向了 DecoratedView
的最近祖先也就是 ChildView
自身的 method
方法,导致递归调用。
解决方案
找到了病因,就不难想到解决方法了。核心思路就是:不要更改被装饰名称的引用。
如果你只是想在内部使用装饰后的新类,可以在装饰器方法中使用 DecoratedView
,而在装饰器返回时 return cls
,以保持引用不变:
def register(cls): decorated = type( 'DecoratedView', (Mixin, cls), {} ) # Do something with decorated return cls
这种方法的缺点是:从外部无法使用 ChildView.another_method
调用 Mixin
上的方法。可如果真的有这样的需求,可以采用另一个解决方案:
def register(cls): cls.another_method = Mixin.another_method return cls
即通过赋值的方式为 cls
添加 Mixin
上的新方法,缺点是较为繁琐。
两种方法各有利弊,要根据实际场景权衡使用。