特殊方法
- 資料模型對 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 * b
及b * 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
,可以避免沒必要的查詢,顯著提昇速度
1 2 |
country_city.setdefault('USA', []).append('New York') |
- 若你實作一個映射的子類別(繼承 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 的貯體是空的,如果太擁擠,它會被複製到一個更大的空間
- 如果兩個物件比對是相同的,那麼雜湊值一定相同
1 2 3 4 5 |
>>> 1 == 1.0 True >>> hash(1) == hash(1.0) True |
- 為了索引效率,雜湊值必須盡可能分散在索引空間
- 從 Python 3.3 開始,str、bytes、datetime 物件的雜湊會被加入隨機的添加值(不同解譯器執行期間會有所不同)來防止 DOS 攻擊(例如 JSON 格式的 API 接口,資料在伺服器端以雜湊表儲存,傳入的攻擊資料可能造成鍵的碰撞)
- 雜湊表演算法:
- 呼叫 hash(search_key) 取得 hash value
- 使用 hash value 最低有效位元作為位移值,檢索貯體
- 找到空的貯體,發出 KeyError
- 從貯體取出 found_key,檢查 search_key == found_key,若不同即發生了雜湊碰撞
- 若發生碰撞,演算法會使用不同方式取得新的位移值,找到下一個貯體
- 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 字元),電腦使用位元組」,中間轉換的過程就是解碼與編碼
1 2 3 4 5 6 7 |
>>> s = 'café' >>> b = s.encode('utf-8') >>> b b'caf\xc3\xa9' <== caf 三個字元屬於可列印的 ASCII 範圍 >>> b.decode('utf-8') 'café' |
- Python3 的
bytes
並非只是將 Python2 的str
換個名稱,也牽涉到密切相關的bytearray
型態 - 二進制序列 (binary sequences) 牽涉到兩種型態:
bytes
跟bytesarray
,在 Python3 每一個二進制序列內的項目都是 0-255 的整數,而不是 Python2 str 那種單字元字串 - 二進制序列的切片會維持同一種型態,如
bytearray
的切片也是bytearray
1 2 3 4 5 6 7 8 9 |
>>> b = bytes('café', encoding='utf_8') >>> b b'caf\xc3\xa9' >>> type(b[:1]) <class 'bytes'> >>> ba = bytearray(b) >>> type(ba[:1]) <class 'bytearray'> |
- 二進制序列是整數序列,但可以被常值標記 (literal notation),例如二進制序列
b'a'
其實是以 61 開頭的整數序列,此處的a
是常值標記,相關的方法為b.hex()
、bytes.fromhex()
- 二進制的常值標記可分成:
- 可列印的 ASCII 字元
- 對應分隔符的位元組,如
b' '
(tab) 對應\t
- 剩下所有的位元組,使用十六進位轉義,如 null byte 對應
\x00
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# 共用記憶體,藉由個別改變 byte 來影響 2-bytes 的資料 NUMS = array.array('h', [0]) MV = memoryview(NUMS).cast('B') def two_bytes_to_int(b1, b2): MV[0], MV[1] = b1, b2 return NUMS[0] >>> two_bytes_to_int(0, 0) 0 >>> two_bytes_to_int(1, 0) 1 >>> two_bytes_to_int(2, 0) 2 >>> two_bytes_to_int(0, 1) 256 >>> two_bytes_to_int(0, 2) 512 >>> two_bytes_to_int(255, 255) -1 >>> two_bytes_to_int(254, 255) -2 >>> two_bytes_to_int(255, 0) 255 >>> two_bytes_to_int(0, 255) -256 >>> two_bytes_to_int(255, 127) 32767 >>> two_bytes_to_int(0, 128) -32768 |
1 2 3 4 5 6 |
# 以可迭代整數來初始化位元組,並顯示其常值標記 >>> nums = array.array('h', [-2, -1, 0, 1, 2]) >>> octets = bytes(nums) >>> octets |
- latin1 編碼又稱為 iso8859_1,是其他編碼的基礎
- 純 ASCII 文字是有效的 UTF-8
1 2 3 4 5 6 7 8 9 10 11 |
# 當字元不在編碼定義中,發生 UnicodeEncodeError >>> 'São'.encode('cp437') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.8/encodings/cp437.py", line 12, in encode return codecs.charmap_encode(input,errors,encoding_map) UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined> >>> 'São'.encode('cp437', errors='replace') b'S?o' |
1 2 3 4 5 6 7 8 9 |
# 不是每個位元組序列都能有效的解碼 >>> s = b'caf\xe9' >>> s.decode('cp1252') 'café' >>> s.decode('utf-8') Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 3: unexpected end of data |
- 對於老舊的編碼,最佳的修正方式是將它們轉換成 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 上,同一個系統可能會使用不同編碼(且彼此不相容),因此更容易遇到編碼錯誤
1 2 3 4 5 6 |
on linux >>> open('output.txt', 'w', encoding='utf-8').write('café') on windows >>> open('output.txt').read() caf� |
- 在 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 編碼成 bytesfsdecode()
:如果傳入 bytes,使用sys.getfilesystemencoding()
作為轉碼器,將 bytes 解碼成 str