前言
這一篇文章是《Fluent Python》- 第一章「資料模型」的閱讀筆記。
Ch1. Python 資料模型
- 可以將資料模型想像成 Python 描述成框架的東西,他決定這個語言本身 building blocks 的接口,像是 sequences, functions, iterators, coroutines, classes, context managers…等。
- 當我們使用任何框架來編寫程式時,會花許多時間來撰寫要被框架呼叫的方法,在使用 Python data model 也是如此,Python interpreter(解譯器)會呼叫特殊的方法來執行基本的物件操作,通常是由特殊的語法觸發。
- 特殊方法的名稱是以雙底線 “__” 作為開頭與結尾,例如:
__getitem__
,obj[key]
語法是__getitem__
特殊方法提供的。 - 當我們希望物件可以支持基本的語言結構並對它做操作時,我們可以實作特殊方法,例如:
- Iteration (including asynchronous iteration using async for) 迭代
- Collections 集合
- 存取 Attribute
- Operator overloading 運算子多載
- Function and method invocation 函式與方法呼叫
- String representation and formatting 字串字串表示與格式化
- Object creation and destruction 物件的建構與解構
- Managed contexts using the with or async with statements 受管理的情境(即with區塊)
- 特殊方法俗稱魔術方法,也稱 dunder 方法,例如:
__getitem__
讀成 “dunder-getitem”.
Python 風格的撲克牌組
Example 1-1. 撲克牌序列
1 | import collections |
- 使用
collections.namedtuple
來建立簡單的類別,並使用它來表示每張卡牌namedtuple
是tuple
的擴展tuple
是 immutable, 建立後就不可更改- 在使用
tuple
時,我們可以利用 index 來存取 tuple 中指定位置的值,但這個 index 只是代表位置,沒有實質的意義,當欄位越來越多時,可讀性也會越來越差,而透過namedtuple
讓我們可以利用名稱來取代 index 值,增加可讀性
1 | >>> beer_card = Card('7', 'diamonds') |
- 範例的重點是 FrenchDeck class, 我們可以使用
len
來取得牌組中的卡片數量:
1 | >>> deck = FrenchDeck() |
- 讀取牌組中的特定卡牌,也正是
__getitem__
特殊方法提供的功能:
1 | >>> deck[0] |
- 隨機選取卡牌:
random.choice
1 | >>> from random import choice |
Python data model 優點:
- 類別的使用者不需要去記各種標準動作的方法名稱
- 如何取得項目數量?
.size()
,.length()
or … ?
- 如何取得項目數量?
- 有豐富的 Python 標準函式庫,避免重複造輪子
但他有更多好處。
以上的 __getitem__
代表 self._cards
的 []
運算子,所以牌組會自動支援 slicing, for example:
1 | # 取得最上面三張牌 |
只要實作 __getitem__
就可以讓牌組變成可迭代的:
1 | >>> for card in deck: # doctest: +ELLIPSIS |
反向迭代:
1 | >>> for card in reversed(deck): # doctest: +ELLIPSIS |
如果某個集合沒有 __contains__
方法, in
運算子就會進行循序掃描。我們可以在 FrenchDeck 中使用 in
:
1 | >>> Card('Q', 'hearts') in deck |
排序:以下範例是按照規則定義撲克牌大小
1 | suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) |
按照大小依序列出牌組:(遞增)
1 | for card in sorted(deck, key=spades_high): # doctest: +ELLIPSIS |
特殊方法的用法
- 特殊方法是要讓 Python 解譯器呼叫的,而不是開發者,所以不能寫
my_obj.__len__()
,而是要用len(my_obj)
. 如果my_obj
是自定義的類別,Python會呼叫你寫的__len__
方法. - 一般情況下,不應該直接呼叫特殊方法,而是呼叫相關的內建函式比較好(例如:len, iter, str 等)。通常開發者比較常呼叫的只有
__init__
, 目的是呼叫自己寫的 initializer. - 解譯器會對內建的型態採取較簡便的做法(例如:list, str, bytearray…等),CPython 的
len()
會回傳 PyVarObject C結構內的 ob_size, 表示記憶體中變數大小的所有內建物件,比呼叫方法快很多。 - 特殊方法通常是私下呼叫的,例如:
for i in x
其實會呼叫iter(x)
, 接著可以的話會呼叫x.__iter__()
。 - 要避免使用 dunder function 來任意建立自訂的屬性,因為雖然目前他不是保留字,但未來有可能具有特殊含義。
模擬數值型態
首先定義一個 Vector class:
1 | from math import hypot |
1 | >>> v1 = Vector(2, 4) |
這裡使用 + 運算子,Python 解譯器會去呼叫 __add__()
.
1 | >>> v = Vector(3, 4) |
這裡使用 abs()
, Python 解譯器會去呼叫 __abs__()
.
也可以使用 * 來執行向量乘法,Python 解譯器會呼叫 __mul__()
.
1 | >>> v * 3 |
字串表示方式
__repr__
特殊方法是讓內建repr
呼叫的,取得物件的字串表示方式,如果沒有實作這個方法,console 中會顯示成<Vector object at 0x10e100070>
.- 互動式的 console 或 debug 程式會對運算式的結果呼叫
repr
, 這裡使用%r
來 format string. __str__
則是讓str()
呼叫的,並讓print()
私下使用,他會回傳方便觀看的格式。- 如果只想實作其中一種,可以選擇
__repr__
, 因為如果沒有自訂的__str__
, Python 會呼叫__repr__
來提供結果。
算術運算子
- 上面的範例使用到 + and *, 以展示
__add__
和__mul__
的基本用法。在這個情況下,方法會回傳新的 Vector instance, 不會修改他們的運算元。 - 在上面的範例中,我們可以將 Vector 乘上數字,但無法將數字乘上 Vector, 這違法乘法的交換特性,會在第十三章的特殊方法
__rmul__
修正這個問題。
自訂類型的布林值
- Python 有 bool type, 但他接受任何 Boolean context 的任何 object, 像是控制 if 或 while 的 expression, 或 and, or, not 運算元。
bool(x)
只會回傳 True or False.- 預設情況下,使用者自訂的 class instance 都會被視為 True, 除非他實作了
__bool__
或__len__
.bool(x)
會呼叫x.__bool__()
並使用它的結果,如果沒有實作__bool__
, Python 就會試著呼叫x.__len__()
, 如果它回傳 0, bool 就會回傳 False.
特殊方法概觀
The Python Language Reference 的 Data model (https://docs.python.org/3/reference/datamodel.html) 中列出了 83 個特殊方法名稱,其中有 47 個的用途是實作算術、位元及比較運算子。
為什麼 len 不是一種方法?
- 在前面的「特殊方法的用法」有提到:如果 x 是內建的型態,CPython 不需要呼叫任何方法,只需要從 C 結構內的欄位讀取。取得集合的長度是蠻常見的動作,因此必須要有效率的操作這種基本類型或其他類型,例如:str, list 或 memoryview.
len
不會被當成方法來呼叫,因為他身為 Python 資料模型的一部份,會受到特殊對待,abs
也是一樣的原理。但是拜特殊方法__len__
所賜,我們也可以讓自訂的物件使用len
.
本章摘要
- 藉由實作特殊方法,讓我們的物件的行為可以像內建類型一樣,讓我們的程式撰寫風格被認為符合 Python 慣例。
- 特殊方法
__repr__
and__str__
, 一個用來進行 debug 和記錄,另一個用來顯示給使用者看,是 Python 物件的基本需求。