Skip to content

Fluent Python 讀書筆記(一)

  • Python

特殊方法

  • 資料模型對 Python 來說是一種「框架」,這個框架(可以想成 Python 的解譯器 Interpreter)會呼叫特定的方法(指 method),這個特定的方法就是 Dunder Method
  • 當你實作了 __getitem__,就代表類別實例可以是可迭代(iterable),且支援了 indexing、slicing、函式庫如 random.choice
  • 若可迭代物件沒有實作 __contains__,在 in 運算子下的預設的行為是循序掃描
  • len() 對於內建型態(如 list、str、bytearray),解譯器並非呼叫 __len__,而是回到底層的 C 結構查詢,因為速度快很多 —— 這也說明了為什麼是 len(collection) 而不是 collection.len,這是一種「在內建物件效率與語言一致性之間取得平衡」
  • for 語句私下是呼叫 iter() 再呼叫 __iter__()
  • 一般而言,你的程式不該自己呼叫特殊方法,而是透過內建方法(如 len、iter、str)
  • 交互式終端與除錯程式會對運算式的結果呼叫 repr()
  • __repr__ 回傳的字串必須精確,盡可能匹配原始碼,好表示「物件如何建立的」以利再次重建
  • 沒有自訂的 __str__,解譯器會呼叫 __repr__ 提供回饋
  • 反向運算子 (reverse operator) 應用在交換律的後備機制如 a * bb * a
  • 擴增賦值運算子 (augmented assignment operator) 意在結合中輟運算 (infix) 與賦值行為,如 a <<= b
  • 「讓使用者使用核心開發人員所使用的工具」,我的理解是,將底層 API 開放給上層使用
  • 「特殊方法其實是魔術方法的相反」,魔術功能通常意指「你不能在自己定義的物件中模擬這些功能」

序列

  • 牢記可變 VS. 不可變、容器序列 VS. 一般序列的不同
  • List comprehension 只做一件事:建構新序列
  • tuple 可以當成「不可變序列」來用,也可當成沒有欄位名稱的「紀錄」(有 unpacking 的優勢)
  • 變數名 _ 翻譯成「啞變數」(使用底線有缺點,會被當成 gettext 的函數別名)
  • 當賦值的對象是切片時,賦值物件必須是 iterable,如 l[2:5] = [20, 30]
  • 序列的 +* 一定都是建立新物件
  • += (原地算法)的特殊方法是 __iadd__, i 代表 in-place,如果 __iadd__沒有被實作的話,Python 會退而呼叫 __add__,可以解讀成「是否真的是原地算法,取決於運算哪種物件」—— 同一個概念可以套用到 *=
  • 檢視 Python 的 bytecode: dis.dis('<your code here>')
  • 重要的 Python API 慣例:當函式或方法就地改變物件(沒有建立新的物件)時,必須回傳 None,簡單的範例是 list.sort(),反例是 sorted(list)
  • 另一種快速的數值儲存方式,是使用 pickle 模組來做物件序列化,使用 pickle.dump 來儲存一個陣列的浮點數幾乎和 array.tofile 一樣快
  • 內建的 memoryview 類別是 Python 的一個廣義 NumPy 陣列結構,可以用它在資料結構間共用記憶體(PIL、SQLite、NumPy 陣列等),而不需要先做複製
  • 移除 deque 中間的項目並不是那麼快,它其實最適合在兩端進行操作
  • queue 函式庫實作的佇列如 Queue、LifoQueue 用來在執行緒之間進行安全通訊
  • Python 的 sort 是使用 Timesort,根據資料的排序狀況來決定採用 MergeSort 或是 InsertionSort

字典與集合

  • 如果一個物件有一個雜湊值(由 __hash__ 定義),而且在它的生命週期中,這個值永遠不變,而且可以和其他物件比較(由 __eq__ 定義),那麼這個物件就是可雜湊化
  • 映射 (map) 型態的的鍵 (key) 必須是可雜湊化 (hashagble),值 (value) 不需要
  • 基本的不可變型態都是可雜湊化的,除了 tuple,因為 tuple 可能包含了不能雜湊的物件
  • 當我們使用 d.setdefault,相較於 d.get,可以避免沒必要的查詢,顯著提昇速度

  • 若你實作一個映射的子類別(繼承 dict),並提供 __missing__ 方法,則 __getitem__ 就會在找不到鍵時呼叫它,而非發出 KeyError
  • __missing__ 只會被 __getitem__ 呼叫(即 d[key] 運算子),並不會影響其他查詢方法,例如 d.get__contains__ (即 in 運算子)
  • key in d.keys() 在 Python3 很有效率,因為是回傳一個 view (類似 set);k.keys() 在 Python2 則是回傳一個陣列
  • 自訂映射型態時,繼承 UserDict 應該比繼承 dict 還更容易,它會將所有鍵存成字串,當有人存取非字串的鍵值時,可避免型態不符的意外
  • 集合 (set) 的底層是雜湊表
  • 集合中的元素必須可以雜湊化,但集合本身不可以雜湊化(除了 frozenset,因此可以在 set 裡放置 frozenset)
  • 空集合沒有常值的表示方式({} 是代表空字典),只能透過 set() 建立
  • 使用常值來建構集合(如 {1, 2, 3}),會比透過建構式還快(如 set([1,2,3])

雜湊表

  • 雜湊表是個稀疏陣列(一定會有空資料格的陣列)
  • 雜湊表內的資料格通稱為貯體 (bucket)
  • 在字典的實現中,每個項目都會有一個貯體,裡面有兩個欄位,分別存放鍵與值的關聯
  • 因為每個貯體大小一樣,因此存取是用「位移值」來做的
  • Python 會試著維持至少 1/3 的貯體是空的,如果太擁擠,它會被複製到一個更大的空間
  • 如果兩個物件比對是相同的,那麼雜湊值一定相同

  • 為了索引效率,雜湊值必須盡可能分散在索引空間
  • 從 Python 3.3 開始,str、bytes、datetime 物件的雜湊會被加入隨機的添加值(不同解譯器執行期間會有所不同)來防止 DOS 攻擊(例如 JSON 格式的 API 接口,資料在伺服器端以雜湊表儲存,傳入的攻擊資料可能造成鍵的碰撞)
  • 雜湊表演算法:
    1. 呼叫 hash(search_key) 取得 hash value
    2. 使用 hash value 最低有效位元作為位移值,檢索貯體
    3. 找到空的貯體,發出 KeyError
    4. 從貯體取出 found_key,檢查 search_key == found_key,若不同即發生了雜湊碰撞
    5. 若發生碰撞,演算法會使用不同方式取得新的位移值,找到下一個貯體
  • dict 是無序的 —— 在插入階段,Python 會判斷雜湊表是否過度擁擠,是否須重新建立至更大空間,來降低碰撞機率,此階段可能引發的是新的雜湊衝突,導致雜湊表的鍵以不同順序排列
  • 在預設情況下,使用者自訂的型態是可以雜湊化的,雜湊值是物件本身的 id()
  • 如果你自訂了 __eq__,那也要實作一個適當的 __hash__,因為要確保當 a == b 時,hash(a) == hash(b) 也要是 True
  • 不要在迭代字典的同時,插入新的項目 —— 因為插入項目可能會觸發雜湊表的重新建構(無法預期),導致鍵的順序更改,而影響迭代的結果

文字 v.s. 位元組

  • 字串 (string) 即一系列的字元; 而針對字元(character)最好的定義是 —— Unicode 字元
  • Python3 的 str、Python2 的 unicode 都是取出 Unicode 字元;Python2 的 str 取出的是原始位元組 (raw bytes)
  • Unicode 標準用特定的位元組(或可看作字碼,總數為 1,114,111 的十進位數字,或可看作 0x10FFFF,源自 0x100000 + 0xFFFF
  • 將字碼轉換成位元組是編碼;將位元組轉換成字碼是解碼,通用的編碼方式是 UTF-8
  • 「人類使用文字(即 Unicode 字元),電腦使用位元組」,中間轉換的過程就是解碼與編碼

  • Python3 的 bytes 並非只是將 Python2 的 str 換個名稱,也牽涉到密切相關的 bytearray 型態
  • 二進制序列 (binary sequences) 牽涉到兩種型態: bytesbytesarray,在 Python3 每一個二進制序列內的項目都是 0-255 的整數,而不是 Python2 str 那種單字元字串
  • 二進制序列的切片會維持同一種型態,如 bytearray 的切片也是 bytearray

  • 二進制序列是整數序列,但可以被常值標記 (literal notation),例如二進制序列 b'a' 其實是以 61 開頭的整數序列,此處的 a 是常值標記,相關的方法為 b.hex()bytes.fromhex()
  • 二進制的常值標記可分成:
    • 可列印的 ASCII 字元
    • 對應分隔符的位元組,如 b' '(tab) 對應 \t
    • 剩下所有的位元組,使用十六進位轉義,如 null byte 對應 \x00

  • latin1 編碼又稱為 iso8859_1,是其他編碼的基礎
  • 純 ASCII 文字是有效的 UTF-8

  • 對於老舊的編碼,最佳的修正方式是將它們轉換成 UTF-8
  • UTF-16 有一種變形 UTF-16LE,是明確的 little-endian,另外一個是 UTF-BE (big-endian),使用這兩個編碼,就不會帶 BOM (byte-order mark) 的前綴
  • Unicode 三明治:在輸入時,盡早將 bytes 解碼成 str;在處理程式邏輯時,只處理 str 物件的文字;在輸出時,盡可能在最後將 str 編碼成 bytes,經典的範例是 open()
  • Python 的 open() 使用本機的預設編碼(Windows 上通常是 cp850、cp1252;Mac、GNU/Linux 則都是 utf-8,通過 locale.getpreferrencoding() 取得),若沒有明確指定,可能會造成特定字元在不同系統上讀取失敗——「需要相容性的程式,永遠都不要依賴預設編碼」
  • 系統的 IO 的重新導向,如果輸出/輸入是檔案,也是由 locale.getpreferrencoding() 決定編碼
  • 在 Windows 上,同一個系統可能會使用不同編碼(且彼此不相容),因此更容易遇到編碼錯誤

  • 在 Python3.3 之前,每個字碼可能使用 2-bytes 或 4-bytes,前者是稱為 narrow build,後者稱為 wide-build
  • 從 Python3.3 開始,當建立一個 str 物件,解譯器會檢查裡面的字元,並挑選最經濟的記憶體配置(若字元都是 latin1,那每個字碼只分配一個 bytes)

Unicode 正規化

  • Unicode 有組合字元,所以字串比較相當複雜,例如 ée\u0301 稱為「典型對等物(canonical equivalents)」,應視為相同——但在 Python 裡,這兩者在長度上就不相同
  • 正規化的方法是使用 unicodedata.normalize,引數 NFC 負責組合字碼、NFD 負責拆解字碼
  • 引數使用 NFKC、NFKD 的話(K 代表相容性),每一個「相容性字元」都會被相容性分解,例如 ½ 會變成 1/2
  • NFKC、NFKD 容易損失或曲解資訊,但可以產生方便的中繼表示,以供搜尋或索引(使用者搜尋 1/2 inch 也希望能看到 ½ inch 的相關內容)
  • 對大多數的應用程式而言,NFC 是最好的正規化形式
  • 要區分大小寫的比較,可使用 str.casefold()
  • Google Search 的其中一個技巧是忽略變音符號,將 Latin 文字改成純 ASCII,例如 ã 替換成 a
  • 在排序時,口音和變音符號不太會影響排序結果
  • 在 Python 中,非 ASCII 文字的標準排序方式,是使用 locale.strxfrm(),在這之前,必須先設定地區 locale.setlocale(),該區域必須事先安裝在作業系統上(區域設定是全域的,應該在應用程式開始前就在環境層級設定好,不應在應用程式內修改)
  • 內建的 locale 函式庫可做國際化排序,但看起來只有 GNU/Linux 有正確支援,除此之外,排序問題可透過 PyUCA 函式庫來解決
  • unicodedata 函式庫提供了完整的資料庫,提供對應字碼與字元名稱的資料表,也有各個字元相互關係的中繼資料。例如,資料表會紀錄某個字元是否可列印、是不是字母、是不是十進位數字
  • 你可以對 bytes 字串使用正規表達式,但必須用 bytes 正規表達式,例如 rb'\w+'
  • GNU/Linux 核心無法精明地處理 Unicode,因此 os 模組接受雙模式: 可使用 str 或 bytes 當引數
  • 為了處理 str 或 bytes 序列的文件名或路徑,os 模組提供特殊的函式:
    • fsencode():如果傳入 str,使用 sys.getfilesystemencoding() 作為轉碼器,將 str 編碼成 bytes
    • fsdecode():如果傳入 bytes,使用 sys.getfilesystemencoding() 作為轉碼器,將 bytes 解碼成 str

發佈留言

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