Skip to content

Fluent Python 讀書筆記(四)

  • Python

介面:從協定到 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

image

  • IndexErrorKeyError 都是 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 來舉例

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 語言實作的內建型態的方法委派,而且只會影響這些型態直接衍生的自訂類別。因此,要擴充功能,你應該繼承 UserDictMutableMapping 這些以 Python 編寫的型態,雖然它們比較慢
  • 多重繼承的「鑽石問題」:彼此之間沒有關係的前代類別實作的方法用了同樣的名稱
  • Python 在遍歷繼承關係圖,會遵循順序(宣告於前方的優先),稱為 MRO: Method Resolution Order,可藉由類別的屬性 __mro__ 取得(以 tuple 方式儲存)
  • 要把呼叫委派給超類別,比較安全的方式是 super(),但有時你也可以繞過 MRO,直接呼叫超類別的方法,例如 super().ping() 改成 A.ping(self)
  • 在類別上直接呼叫實例方法時,你必須傳一個實例進去作為 self 引數,這種方法呼叫是未綁定的 (unbound method)
  • 在 Python 的標準函式庫中,最常見到多重繼承的地方就是 collections.abc

繼承 101

  1. 搞清楚為什麼要繼承,是要實作介面?還是避免重複的程式碼?後者可以考慮改用 Mixin 來取代
  2. 介面應該要是個明確的 ABC
  3. Mixin 只會將方法打包,來做再利用,且每一個 Mixin 應該只提供一個具體的行為
  4. Mixin 的命名應該要是 “…Mixin”
  5. Mixin 有多個是好的,但具體的超類別不要超過一個
  6. 可以將一個具體類別和多個特定的 Mixin 事先整合成「聚合類別(aggregate class)」,在使用上可以顯得更簡化且明確
  7. 「比起類別繼承,物件組合更好」——不要濫用繼承

欣賞一下 Django 的視圖系統,好好體會體會

image


  • 身為應用程式開發者,我們編寫的大部分類別都是末端類別,很少會去建立 ABC 及框架
  • 當你在開發應用程式時,發現自己在建構 ABC 及類別的 UML,你可能在重造輪子或使用不良的框架,請去尋找好的函式庫及替代方案

運算子多載

  • 運算子多載透過中綴運算子(例如 +|)或一元運算子(例如 -~),讓不同型態的物件、自訂的物件可以相互合作
  • Python 對運算子多載的限制:1) 內建型態無法自訂多載 2) 無法自訂運算子 3) 有些運算子不能被多載,如 isandornot
  • __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),會有更好的安全性跟效能;而好的、受限的運算子多載則可讓程式碼更容易編寫與閱讀

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *