介面:從協定到 ABC
- 「抽象類別代表介面」
- Python 自 2.6 版本之後加入 ABC (abstact base class),大多被定義在
collections.abc
模組 - 當你需要實作介面時,第一步是將它們當成超類別 (superclasses),ABC 會檢查具體子類別是否符合這個介面
- ABC 與描述器 (descriptors)、中繼類別(metaclasses)一樣,是建構框架的工具,過度使用 ABC 的風險是非常高的
- 可以把介面想成「某個物件的公用方法的子集合(subsets)」,這個子集合可以在系統中發揮具體的作用(常在文件看到 “a file-like object”、”an iterable” 的字眼都是在指涉這件事)
- 協定(protocal) 是非正式的介面,只由文件與慣例定義,無法被強制實施,例如:選擇只實作序列協定的某些方法如
__getitem__
,而不是繼承abc.Sequence
- Python 資料模型的哲學,就是盡可能地與基本協定合作
isinstance(obj, cls)
沒有那麼糟,只要 cls 是一個 ABC- 所謂的 goose typing ,是相對於協定的 duck typing,鼓勵我們可以去實作 ABC 的介面(透過繼承而非自造輪子)
- Python ABC 有類別方法 register 可以讓使用者「宣告」某個類別是 ABC 的一個「虛擬子類別 (virtual subclasses)」,而不用實際的繼承,簡單來說就是讓 Python 相信我們會實作介面而不實際檢查(如果有任何問題,就讓在執行階段拋出例外吧)
- 除了透過函式呼叫來註冊,在 Python 3.4 之後提供了類別修飾器
@<ABC classname>.register
- 有些子類別不一定要明確的註冊或繼承,也可以成為特定 ABC 的子類別,例如
__len__
之於abc.Sized
(背後是透過__subclasshook__
來實現的,類似的實作少之又少) - 「不要在程式中自訂 ABC 或 metaclass」—— 從 ABC 繼承方法比實作需要的方法還要好,ABC 的目的是封裝因為框架而產生的一般性、抽象概念,例如這是一個「序列」與「確切的數字」
- 「ABC 的流行可能是個災難,它對語言施加過度的儀式」
numbers
裡面定義了數值的 ABC,最頂層的超類別是numbers.Number
IndexError
、KeyError
都是LookupError
的子類別- 宣告 ABC 有兩種方式: 1. 繼承
abc.ABC
(3.4 之後才加入) 2. 指定metaclass=abc.ABCMeta
(3~3.4 的限定作法) - 諸如
@abstractclassmethod
的冗員裝飾器已被 ABC 棄用,要用的話,只要單純疊加@classmethod
、@abc.abstractmethod
即可(要注意順序) - 「雖然 ABC 有助於型態檢查,但不應該過度使用它。Python 的核心是動態語言,到處限制型態,可能會讓程式變成沒必要的複雜」
- 型態提示 (type hints) 是註釋的一種,可以在函式定義中指名參數的型態及回傳何種型態,沒有強致力
強 vs 弱型態
如果語言很少執行隱式型態轉換,那它就是一個強型態
Java、C++、Python 都是強型態;PHP、JavaScript、Perl 都是弱型態
靜態 vs 動態型態
如果型態檢查會在編譯階段執行,語言那就是靜態型態;如果在執行階段發生,就是動態型態
靜態型態須要型態宣告,可讓工具(編譯器、IDE)更容易分析程式碼來找出錯誤,並提供最佳化、重構等服務;動態型態可增加「再利用」的機會,減少行數、並讓介面自然而然成為協定,而不需要提早實行
為什麼弱型態不好?以 JavaScript 來舉例
1 2 3 4 5 6 |
'' == '0' // false 0 == '' // true 0 == '0' // true '' < 0 // false '' < '0' // true |
C++、Java、Go、Ruby 的介面
C++ 一樣是以抽象類別來指定介面,可以多重繼承,但容易被濫用
Java 不讓類別可以多重繼承,因此抽象類別不能作為界面(只能繼承一個不夠)。 Java 用的是 interface 語言結構,讓類別可以實作一個以上的界面
Go 裡面沒有繼承。你可以定義介面,但你不需要(也不能)明說「XX型態實作了OO介面」,編譯器會自動判斷。所以 Go 裡面的東西可稱為「靜態的 duck typing」,因為介面是在編譯階段檢查的。與 Python 相較,Go 就好像每個 ABC 都實作了 __subclasshook__
來檢查函式名稱與簽章,而不是明確繼承或註冊 ABC
Ruby 只有 duck typing,或許未來會有正式的介面
- Monkey Patching 實用的地方在於讓一個類別在執行階段實作一個協定,這種和目標緊耦合的關係,容易讓程式變得脆弱
- Python 的內建型態是不給 monkey patch 的
- 「或許 goose typing 正準備超越 duck typing」
繼承: 好或不好
- 你可以繼承內建型態,但內建型態的程式碼(以 C 寫成)不會呼叫被自訂類別覆寫的特殊方法,例如繼承
dict
且覆寫__setitem__
會沒有作用。這是為了速度而犧牲擴充性的作法。這種問題只會發生在以 C 語言實作的內建型態的方法委派,而且只會影響這些型態直接衍生的自訂類別。因此,要擴充功能,你應該繼承UserDict
、MutableMapping
這些以 Python 編寫的型態,雖然它們比較慢 - 多重繼承的「鑽石問題」:彼此之間沒有關係的前代類別實作的方法用了同樣的名稱
- Python 在遍歷繼承關係圖,會遵循順序(宣告於前方的優先),稱為 MRO: Method Resolution Order,可藉由類別的屬性
__mro__
取得(以 tuple 方式儲存) - 要把呼叫委派給超類別,比較安全的方式是
super()
,但有時你也可以繞過 MRO,直接呼叫超類別的方法,例如super().ping()
改成A.ping(self)
- 在類別上直接呼叫實例方法時,你必須傳一個實例進去作為 self 引數,這種方法呼叫是未綁定的 (unbound method)
- 在 Python 的標準函式庫中,最常見到多重繼承的地方就是
collections.abc
繼承 101
- 搞清楚為什麼要繼承,是要實作介面?還是避免重複的程式碼?後者可以考慮改用 Mixin 來取代
- 介面應該要是個明確的 ABC
- Mixin 只會將方法打包,來做再利用,且每一個 Mixin 應該只提供一個具體的行為
- Mixin 的命名應該要是 “…Mixin”
- Mixin 有多個是好的,但具體的超類別不要超過一個
- 可以將一個具體類別和多個特定的 Mixin 事先整合成「聚合類別(aggregate class)」,在使用上可以顯得更簡化且明確
- 「比起類別繼承,物件組合更好」——不要濫用繼承
欣賞一下 Django 的視圖系統,好好體會體會
- 身為應用程式開發者,我們編寫的大部分類別都是末端類別,很少會去建立 ABC 及框架
- 當你在開發應用程式時,發現自己在建構 ABC 及類別的 UML,你可能在重造輪子或使用不良的框架,請去尋找好的函式庫及替代方案
運算子多載
- 運算子多載透過中綴運算子(例如
+
、|
)或一元運算子(例如-
、~
),讓不同型態的物件、自訂的物件可以相互合作 - Python 對運算子多載的限制:1) 內建型態無法自訂多載 2) 無法自訂運算子 3) 有些運算子不能被多載,如
is
、and
、or
、not
__radd__
方法被稱為反射 (reflected) 或反向(reversed)版的__add__
,因為它們被呼叫的地方是在運算子右邊__iadd__
屬於擴增賦值運算子 (augmented assignment operator) ,意指原地算法 (inplace) 版的__add__
,此特殊方法應該要回傳 self。不可變型態不應該支援此特殊方法- 如果中綴運算子特殊方法因為型態不相容而無法回傳有效的結果,應該回傳
NotImplemented
(一個解譯器能識別的常值),藉此讓Python 試著去呼叫反向的運算子。若反向的運算子也回傳NotImplemented
,那 Python 會採取最後手段:發出TypeError
- 同上,特殊的情況是
==
:Python 會在最後多做一個嘗試:比較物件 ID NotImplementedError
是一種例外,由抽象類別的虛設方法發出,來警告它們必須被子類別實作- 運算子
@
意指計算內積,支援__matmul__
系列的特殊方法(命名自 matrix multiplication) list
的+=
運算等同於list.extend()
- 一般來說,程式應該利用 dynamic typing,直接嘗試運算,再處理例外就好,但是回歸到運算子的多載的特殊方法的實作,
isinstance
測試在裡頭其實滿實用的(受益於 goose typing) - Python 不使用雙重指派(double dispatching),而是使用「順向」與「反向」運算子來實現運算,對自訂型態來說支援度更好
- 語言不使用運算子多載也有好處(例如 Java、Go),會有更好的安全性跟效能;而好的、受限的運算子多載則可讓程式碼更容易編寫與閱讀