本文首发于个人博客,转载请注明出处python
《神坑》系列将会不按期更新一些可遇而不可求的坑
防止他人入坑,也防止本身再次入坑django
现有两个 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
),一般可极大地减小定义类似类时的工做量:code
class Mixin(object): pass def register(cls): return type( 'DecoratedView', (Mixin, cls), {} )
这个装饰器为被装饰类附加上一个额外的父类 Mixin
,以增添自定义的功能。blog
完整的代码以下:继承
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
神秘地失效了呢?get
为了验证 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
上的新方法,缺点是较为繁琐。
两种方法各有利弊,要根据实际场景权衡使用。