Python super()使用注意事項

2020-07-16 10:05:02
Python 中,由於基礎類別不會在 __init__() 中被隱式地呼叫,需要程式設計師顯式呼叫它們。這種情況下,當程式中包含多重繼承的類層次結構時,使用 super 是非常危險的,往往會在類的初始化過程中出現問題。

混用super與顯式類呼叫

分析如下程式,C 類使用了 __init__() 方法呼叫它的基礎類別,會造成 B 類被呼叫了 2 次:
class A:
    def __init__(self):
        print("A",end=" ")
        super().__init__()
class B:
    def __init__(self):
        print("B",end=" ")
        super().__init__()
class C(A,B):
    def __init__(self):
        print("C",end=" ")
        A.__init__(self)
        B.__init__(self)
print("MRO:",[x.__name__ for x in C.__mro__])
C()
執行結果為:

MRO: ['C', 'A', 'B', 'object']
C A B B

出現以上這種情況的原因在於,C 的範例呼叫 A.__init__(self),使得 super(A,self).__init__() 呼叫了 B.__init__() 方法。換句話說,super 應該被用到整個類的層次結構中。

但是,有時這種層次結構的一部分位於第三方程式碼中,我們無法確定外部包的這些程式碼中是否使用 super(),因此,當需要對某個第三方類進行子類化時,最好檢視其內部程式碼以及 MRO 中其他類的內部程式碼。

不同種類的引數

使用 super 的另一個問題是初始化過程中的引數傳遞。如果沒有相同的簽名,一個類怎麼能呼叫其基礎類別的 __init__() 程式碼呢?這會導致下列問題:
class commonBase:
    def __init__(self):
        print("commonBase")
        super().__init__()

class base1(commonBase):
    def __init__(self):
        print("base1")
        super().__init__()

class base2(commonBase):
    def __init__(self):
        print("base2")
        super().__init__()

class myClass(base1,base2):
    def __init__(self,arg):
        print("my base")
        super().__init__(arg)
myClass(10)
執行結果為:

my base
Traceback (most recent call last):
  File "C:UsersmengmaDesktopdemo.py", line 20, in <module>
    myClass(10)
  File "C:UsersmengmaDesktopdemo.py", line 19, in __init__
    super().__init__(arg)
TypeError: __init__() takes 1 positional argument but 2 were given

一種解決方法是使用 *args 和 **kwargs 包裝的引數和關鍵字引數,這樣即使不使用它們,所有的建構函式也會傳遞所有引數,如下所示:
class commonBase:
    def __init__(self,*args,**kwargs):
        print("commonBase")
        super().__init__()

class base1(commonBase):
    def __init__(self,*args,**kwargs):
        print("base1")
        super().__init__(*args,**kwargs)

class base2(commonBase):
    def __init__(self,*args,**kwargs):
        print("base2")
        super().__init__(*args,**kwargs)

class myClass(base1,base2):
    def __init__(self,arg):
        print("my base")
        super().__init__(arg)
myClass(10)
執行結果為:

my base
base1
base2
commonBase

不過,這是一種很糟糕的解決方法,由於任何引數都可以傳入,所有建構函式都可以接受任何型別的引數,這會導致程式碼變得脆弱。另一種解決方法是在 MyClass 中顯式地使用特定類的 __init__() 呼叫,但這無疑會導致第一種錯誤。

總結

如果想要避免程式中出現以上的這些問題,這裡給出幾點建議:
  • 盡可能避免使用多繼承,可以使用一些設計模式來替代它;
  • super 的使用必須一致,即在類的層次結構中,要麼全部使用 super,要麼全不用。混用 super 和傳統呼叫是一種混亂的寫法;
  • 如果程式碼需要相容 Python 2.x,在 Python 3.x 中應該顯式地繼承自 object。在 Python 2.x 中,沒有指定任何祖先地類都被認定為舊式類。
  • 呼叫父類別時應提前檢視類的層次結構,也就是使用類的 __mro__ 屬性或者 mro() 方法檢視有關類的 MRO。