神坑·Python 裝飾類無限遞迴

發表於2016-08-01

簡化版問題
現有兩個 View 類:

以及一個用於修飾該類的裝飾器函式 register——用於裝飾類的裝飾器很常見(如 django.contrib.adminregister),通常可極大地減少定義相似類時的工作量:

這個裝飾器為被裝飾類附加上一個額外的父類 Mixin,以增添自定義的功能。

完整的程式碼如下:

看上去似乎沒什麼問題。然而一旦呼叫 View().method(),卻會報出詭異的 無限遞迴 錯誤:

【一臉懵逼】

猜想 & 驗證

從 Traceback 中可以發現:是 super(ChildView, self).method() 在不停地呼叫自己——這著實讓我吃了一驚,因為 按理說 super 應該沿著繼承鏈查詢父類,可為什麼在這裡 super 神祕地失效了呢?

為了驗證 super(...).method 的指向,可以嘗試將該語句改為 print(super(ChildView, self).method),並觀察結果:

輸出表明: method 的指向確實有誤,此處本應為 View.method

super 是 python 內建方法,肯定不會出錯。那,會不會是 super 的引數有誤呢?

super 的簽名為 super(cls, instance),巨集觀效果為 遍歷 cls 的繼承鏈查詢父類方法,並以 instance 作為 self 進行呼叫。如今查詢結果有誤,說明 繼承鏈是錯誤的,因而極有可能是 cls 出錯。

因此,有必要探測一下 ChildView 的指向。在 method 中加上一句: print(ChildView)

原來,作用域中的 ChildView 已經被改變了。

真相

一切都源於裝飾器語法糖。我們回憶一下裝飾器的等價語法:

等價於

這說明:裝飾器會更改該作用域內被裝飾名稱的指向

這本來沒什麼,但和 super 一起使用時卻會出問題。通常情況下我們會將本類的名稱傳給 super(在這裡為 ChildView),而本類名稱和裝飾器語法存在於同一作用域中,從而在裝飾時被一同修改了(在本例中指向了子類 DecoratedView),進而使 super(...).method 指向了 DecoratedView 的最近祖先也就是 ChildView 自身的 method 方法,導致遞迴呼叫。

解決方案

找到了病因,就不難想到解決方法了。核心思路就是:不要更改被裝飾名稱的引用

如果你只是想在內部使用裝飾後的新類,可以在裝飾器方法中使用 DecoratedView,而在裝飾器返回時 return cls,以保持引用不變:

這種方法的缺點是:從外部無法使用 ChildView.another_method 呼叫 Mixin 上的方法。可如果真的有這樣的需求,可以採用另一個解決方案:

即通過賦值的方式為 cls 新增 Mixin 上的新方法,缺點是較為繁瑣。

兩種方法各有利弊,要根據實際場景權衡使用。

相關文章