本文首發於個人博客,轉載請註明出處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
上的新方法,缺點是較爲繁瑣。
兩種方法各有利弊,要根據實際場景權衡使用。