簡化版問題
現有兩個 View
類:
1 2 3 4 5 6 7 8 9 10 11 |
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
),通常可極大地減少定義相似類時的工作量:
1 2 3 4 5 6 7 8 9 10 |
class Mixin(object): pass def register(cls): return type( 'DecoratedView', (Mixin, cls), {} ) |
這個裝飾器為被裝飾類附加上一個額外的父類 Mixin
,以增添自定義的功能。
完整的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Mixin(object): pass def register(cls): return type( cls.__name__, (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() |
看上去似乎沒什麼問題。然而一旦呼叫 View().method()
,卻會報出詭異的 無限遞迴 錯誤:
1 2 3 4 5 6 7 8 |
# ... 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)
,並觀察結果:
1 |
<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)
:
1 |
<class '__main__.DecoratedView'> |
原來,作用域中的 ChildView
已經被改變了。
真相
一切都源於裝飾器語法糖。我們回憶一下裝飾器的等價語法:
1 2 3 |
@decorator class Class: pass |
等價於
1 2 3 4 |
class Class: pass Class = decorator(Class) |
這說明:裝飾器會更改該作用域內被裝飾名稱的指向。
這本來沒什麼,但和 super
一起使用時卻會出問題。通常情況下我們會將本類的名稱傳給 super
(在這裡為 ChildView
),而本類名稱和裝飾器語法存在於同一作用域中,從而在裝飾時被一同修改了(在本例中指向了子類 DecoratedView
),進而使 super(...).method
指向了 DecoratedView
的最近祖先也就是 ChildView
自身的 method
方法,導致遞迴呼叫。
解決方案
找到了病因,就不難想到解決方法了。核心思路就是:不要更改被裝飾名稱的引用。
如果你只是想在內部使用裝飾後的新類,可以在裝飾器方法中使用 DecoratedView
,而在裝飾器返回時 return cls
,以保持引用不變:
1 2 3 4 5 6 7 8 9 10 11 |
def register(cls): decorated = type( 'DecoratedView', (Mixin, cls), {} ) # Do something with decorated return cls |
這種方法的缺點是:從外部無法使用 ChildView.another_method
呼叫 Mixin
上的方法。可如果真的有這樣的需求,可以採用另一個解決方案:
1 2 3 4 |
def register(cls): cls.another_method = Mixin.another_method return cls |
即通過賦值的方式為 cls
新增 Mixin
上的新方法,缺點是較為繁瑣。
兩種方法各有利弊,要根據實際場景權衡使用。