神坑·Python 裝飾類無限遞歸

本文首發於個人博客轉載請註明出處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.adminregister),一般可極大地減小定義類似類時的工做量: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 上的新方法,缺點是較爲繁瑣。

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

相關文章
相關標籤/搜索