Python 中的新式類和經典類的區別?

1. 版本支持 / 寫法差異

在Python 2.x 中

如果你至今使用的還是 Python 2.x,那麼你需要了解一下,在Python 2.x中存在着兩種類:經典類和新式類。

什麼是經典類?

# 不繼承自object
class Ming:  
    pass

什麼是新式類?

# 顯示繼承object
class Ming(object):  
    pass

在Python 3.x 中

如果你已經摒棄了Python 2.x,而已經完全投入了Python 3.x的懷抱,那你可能不需要關心,因爲在Python3.x中所有的類都是新式類(所有類都(顯示/隱式)繼承自object)。

有如下三種寫法,它們是等價的。

class Ming:
    pass

class Ming():
    pass

class Ming(object):
    pass

2. 使用方法 / 獨特屬性

經典類無法使用super()

經典類的類型是 classobj

新式類的類型是 type,保持class與type的統一。

新式類增加了__slots__內置屬性, 可以把實例屬性的種類鎖定到__slots__規定的範圍之中.
新式類增加了__getattribute__方法,在獲取屬性時可以進入此方法。而經典類不會。

# 經典類:py2.7
class Kls01:
    def __getattribute__(self, *args, **kwargs):
        print("MING - 01")

# 新式類
class Kls02(object):
    def __getattribute__(self, *args, **kwargs):
        print("MING - 02")

kls02 = Kls02()
kls02.name

kls01 = Kls01()
kls01.name

輸出如下

MING - 02
Traceback (most recent call last):
  File "F:/Python Script/Tornado/yang.py", line 13, in <module>
    kls01.name
AttributeError: Kls01 instance has no attribute 'name'

3. MRO 查找算法的演變

經典類中

經典類,MRO採用的是深度優先算法。

爲了驗證,這個繼承順序。在 Python2.x 中可以藉助自帶模塊 inspect 來檢驗。

import inspect

class A:pass
class B(A):pass
class C(A):pass
class D(B, C):pass

print inspect.getmro(D)

輸出如下,可以看出,繼承順序是 D -> B -> A -> C

(<class __main__.D at 0x0000000005836A08>, 
<class __main__.B at 0x0000000005836768>, 
<class __main__.A at 0x00000000058368E8>, 
<class __main__.C at 0x0000000005836AC8>)

非常好理解,但是在菱形繼承時,方法的調用會出現問題。

假設 d 是 D 的一個實例,那麼執行 d.show()是調用 A.show() 呢 還是調用 C.show()呢?

在經典類中,由於是深度優先,所以是會選擇 A.show()。但是很明顯,C.show() 是 A.show() 的更具體化版本(顯示了更多的信息),但我們的x.show() 沒有調用它,而是調用了 A.show(),這顯然是不合理的。所以這纔有了後來的一步一步優化。

新式類中

爲解決經典類 MRO 所存在的問題,Python 2.2 針對新式類提出了一種新的 MRO 計算方式:在定義類時就計算出該類的 MRO 並將其作爲類的屬性。

Python 2.2 的新式類 MRO 計算方式和經典類 MRO 的計算方式非常相似:它仍然採用從左至右的深度優先遍歷,但是如果遍歷中出現重複的類,只保留最後一個。重新考慮上面「菱形繼承」的例子:

同樣地,我們也來驗證一下。另說明,在新式類中,除用inspect外,可以直接通過__mro__屬性獲取類的 MRO。

import inspect

class A(object):pass
class B(A):pass
class C(A):pass
class D(B, C):pass

# 或者通過 D.__mro__ 查找
print inspect.getmro(D)

輸出如下,可以看出,繼承順序變成了 D -> B -> C -> A

(<class '__main__.D'>, 
<class '__main__.B'>, 
<class '__main__.C'>, 
<class '__main__.A'>, 
<type 'object'>)

這下,菱形問題解決了。

再來看一個複雜一點的例子。

如果只依靠上面的算法,我們來一起算下,其繼承關係是怎樣的。

  1. 首先進行深度遍歷:[C, A, X, object, Y, object, B, Y, object, X, object];
  2. 然後,只保留重複元素的最後一個:[C, A, B, Y, X, object]。

同樣來驗證一下。

class X(object): pass
class Y(object): pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass

print(C.__mro__)

輸出報錯,它告訴我們 X,Y 具有二義性的繼承關係(這是從Python 2.3後的 C3算法 纔有的)。

Traceback (most recent call last):
  File "F:/Python Script/Tornado/yang.py", line 7, in <module>
    class C(A, B): pass
TypeError: Error when calling the metaclass bases
    Cannot create a consistent method resolution
order (MRO) for bases X, Y

具體爲什麼會這樣,我們來看一下。

對於 A 來說,其搜索順序爲[A, X, Y, object];
對於 B,其搜索順序爲 [B, Y, X, object];
對於 C,其搜索順序爲[C, A, B, X, Y, object]。

我們會發現,B 和 C 中 X、Y 的搜索順序是相反的!也就是說,當 B 被繼承時,它本身的行爲竟然也發生了改變,這很容易導致不易察覺的錯誤。此外,即使把 C 搜索順序中 X 和 Y 互換仍然不能解決問題,這時候它又會和 A 中的搜索順序相矛盾。

對於複雜一點的繼承關係,我們在寫代碼的時候最好做到心中有數。接下來,就教教你,如何在層層複雜的繼承關係中,計算出繼承順序。

例如下面這張圖。

計算過程,會採用一種 merge算法。它的基本思想如下:

  1. 檢查第一個列表的頭元素(如 L[B1] 的頭),記作 H。
  2. 若 H 未出現在其它列表的尾部,則將其輸出,並將其從所有列表中刪除,然後回到步驟1;否則,取出下一個列表的頭部記作 H,繼續該步驟。
  3. 重複上述步驟,直至列表爲空或者不能再找出可以輸出的元素。如果是前一種情況,則算法結束;如果是後一種情況,說明無法構建繼承關係,Python 會拋出異常。

你可以在草稿紙上,參照上面的merge算法,寫出如下過程

L[object] = [object]
L[D] = [D, object]
L[E] = [E, object]
L[F] = [F, object]
L[B] = [B, D, E, object]
L[C] = [C, D, F, object]
L[A] = [A] + merge(L[B], L[C], [B], [C])
     = [A] + merge([B, D, E, object], [C, D, F, object], [B], [C])
     = [A, B] + merge([D, E, object], [C, D, F, object], [C])
     = [A, B, C] + merge([D, E, object], [D, F, object])
     = [A, B, C, D] + merge([E, object], [F, object])
     = [A, B, C, D, E] + merge([object], [F, object])
     = [A, B, C, D, E, F] + merge([object], [object])
     = [A, B, C, D, E, F, object]

當然,可以用代碼驗證類的 MRO,上面的例子可以寫作:

class D(object): pass
class E(object): pass
class F(object): pass
class B(D, E): pass
class C(D, F): pass
class A(B, C): pass

A.__mro__

輸出如下

(<class '__main__.A'>, 
<class '__main__.B'>, 
<class '__main__.C'>, 
<class '__main__.

附錄:參考文章


文末福利

本人原創的 《PyCharm 中文指南》一書前段時間一經發布,就火爆了整個 Python 圈,發佈僅一天的時間,下載量就突破了 1000 ,並且在當天就在 Github 上就收穫了數百的 star,截至目前,下載量已經破萬。

這本書一共將近 200 頁內含大量的圖解製作之精良,值得每個 Python 工程師 人手一份。

爲方便你下載,我將這本書上傳到 百度網盤上了,你可以自行獲取。

鏈接:https://pan.baidu.com/s/1-NzATHFtaTV1MQzek70iUQ

密碼:mft3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章