《流畅的Python第二版》读书笔记——Python数据模型
引言
這是《流暢的Python第二版》搶先版的讀書筆記。Python版本暫時用的是python-3.8。為了使開發更簡單、快捷,本文使用了JupyterLab。
Python解釋器調用一些特殊方法來進行基本的對象操作,這種特殊方法通常有特殊的寫法。比如__getitem__,如果你實現了該方法,就可以通過obj[key]來觸發obj.__getitem__(key)方法。
這些特殊方法名能讓你自己的對象實現和支持以下的語言構架,并與之交互:
- 集合類
- 屬性訪問
- 迭代
- 運算符重載
- 函數和方法的調用
- 對象的創建和銷毀
- 使用await的異步編程
- 字符串表示形式和格式化
- 管理上下文(即 with 塊)
有些人也稱這些特殊方法為魔術方法(magic method)。
A Pythonic Card Deck
本節這個例子雖然簡單,但是它通過實現__getitem__和__len__方法來展示特殊方法的強大。
frenchdeck.py:
import collections# 利用namedtuple構造一個簡單的表示紙牌的類 Card = collections.namedtuple('Card', ['rank', 'suit'])class FrenchDeck:ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 點數suits = 'spades diamonds clubs hearts'.split() # 花數def __init__(self):self._cards = [Card(rank, suit) for suit in self.suitsfor rank in self.ranks]def __len__(self):return len(self._cards)def __getitem__(self, position):return self._cards[position]我們可以像下面這樣很方便的構造Card對象:
from frenchdeck import Card,FrenchDeckbeer_card = Card('7', 'diamonds') beer_card
但本例的重點是FrenchDeck類(一疊紙牌),它雖然很小,但很強大。像標準的Python集合一樣,可以調用len()函數來獲取對象中Card實例的數量:
還可以從一疊紙牌中抽取第一張和最后一張,很簡單deck[0]、deck[-1]即可,這是__getitem__方法提供的:
Python提供了一個函數從序列中隨機獲取元素:random.choice,我們可以直接用在deck實例上:
現在我們已經看到了實現特殊方法來利用Python數據模型的兩個好處:
- 作為你的類的用戶,無需記憶標準操作的各種名稱(比如如何獲取元素個數,通過.size()還是.length())。
- 更加方便地利用Python標準庫,避免重復造輪子。就像random.choice函數一樣。
因為__getitem__方法把[]操作委托(delegates)給了self._cards列表,我們的deck自動支持切片。下面展示了如何查看牌堆中前三張牌,然后通過從第13張牌開始(索引是12),每隔13張牌抽一張牌,來選取牌A:
deck[:3] deck[12::13]
而且,僅僅是實現了__getitem__特殊方法,我們的deck還可以迭代:
(上圖只截取部分)
同時也可以反向迭代:
迭代通常是隱式的,如果一個集合沒有實現__contains__方法,那么in運算符就會執行一個順序掃描。
于是,in 運算符可以用在我們的FrenchDeck 類上,因為它是可迭代的:
還可以實現排序,比如用點數來判斷紙牌大小,2最小、A最大;同時黑桃(spades)最大、紅桃(hearts)次之、方塊(diamonds)再次、梅花(clubs)最小。下面就是按照這個規則來排序的函數,約定梅花2的大小是0,黑桃A是51:
有了這個函數,就你可以對牌堆進行升序排序了:
for card in sorted(deck, key=spades_high):print(card)
通過實現__len__和__getitem__這兩個特殊方法,FrenchDeck就跟一個Python自有的序列數據類型一樣,可以體現出Python的核心語言特性(例如迭代和切片)。同時這個類還可以用于標準庫中random.choice、reversed和sorted這些函數。另外,對于組合的運用是的__len__和__getitem__的具體實現可以委托給self._cards這個list對象。
如何使用特殊方法
首先要知道,特殊方法是被Python解釋器而不是你調用的,你不能寫my_object.__len__(),而是寫len(my_object):Python會調用你實現的__len__方法。
然后如果是Python內置類型,比如列表、字符串、字節序列(bytearray)等,那么解釋器會走捷徑,__len__實際上會直接返回PyVarObject里的ob_size屬性,直接讀取這個值比調用一個方法要快很多。
很多時候,特殊方法調用是隱式的。比如語句for i in x:實際上會導致調用iter(x),而背后解釋器會調用x.__iter__(),如果有的這個方法的話。否則像FrenchDeck例子一樣,調用x.__getitem__()。
通常你無需直接調用特殊方法,除非有大量的元編程存在。唯一的例外可能是__init__方法。
通過內置函數來使用特殊方法是最好的選擇。
不要想當然的隨意添加特殊方法,比如__foo__之類的,因為雖然這個名字現在沒有被Python內部使用,以后就不一定了。
模擬數值類型
利用特殊方法,可以讓自定義對象通過加號+等運算符進行運算。
我們實現一個二維向量類vector,就是我們數學中的向量。
上圖是一個向量加法的例子:Vector(2,4)+Vector(2,1) = Vector(4,5)
我們實現的向量類應該能做這樣的加法,通過+運算符:
>>> v1 = Vector(2,4) >>> v2 = Vector(2,1) >>> v1 + v2 Vector(4,5)其中+運算符得到的結果也是一個向量,并且打印出來的結果很友好。
我們的向量也應該支持abs函數,返回的是向量的模·:
>>> v = Vector(3,4) >>> abs(v) # sqrt(3**2+4**2) 5.0還可以利用*運算符實現向量的標量乘法(向量與數的乘法):
>>> v * 3 Vector(9,12) >>>abs(v * 3) 15.0下面就來實現這樣一個Vector類,上面提到的操作在代碼中是用__repr__、__abs__、__add__和__mul__實現的:
""" vector2d.py: 一個展示一些特殊方法的簡單類Addition::>>> v1 = Vector(2, 4)>>> v2 = Vector(2, 1)>>> v1 + v2Vector(4, 5)Absolute value::>>> v = Vector(3, 4)>>> abs(v)5.0Scalar multiplication::>>> v * 3Vector(9, 12)>>> abs(v * 3)15.0"""import mathclass Vector:def __init__(self, x=0, y=0):self.x = xself.y = ydef __repr__(self):return f'Vector({self.x!r}, {self.y!r})'def __abs__(self):return math.hypot(self.x, self.y)def __bool__(self):return bool(abs(self))def __add__(self, other):x = self.x + other.xy = self.y + other.yreturn Vector(x, y)def __mul__(self, scalar):return Vector(self.x * scalar, self.y * scalar)乍一看我們實現了6個特殊方法,包括__init__初始化方法。下面來介紹其他幾個方法。
字符串表示
__repr__特殊方法被Python內置的repr函數調用,來展示一個對象的字符串形式。類似于Java中的toString()。
交互式控制臺和調試程序(debugger)用repr函數來獲取字符串表示形式。傳統的字符串格式中使用%運算符,現在有一種新的字符串格式化語法!r用在str.format方法中(也可以在要格式的字符串前加個f,就如上面代碼所示的)。也是利用repr把!r替換為字符串。
一些例子:
"Harold's a clever {0!s}" # 調用 str() , s for str "Bring out the holy {name!r}" # 調用repr(),r for repr注意在我們的__repr__實現中,使用了!r來獲取對象各個屬性的標準字符串表示。這是一個好習慣,它展示了Vector(1,2)和Vector('1','2')的不同。后者會在我們的定義中報錯,因為構造函數只接受數值,而不是字符串。
其實作者在第一版中說使用%r是一個好習慣。
__repr__所返回的字符串應該準確且無歧義,并盡可能表達出如何用代碼創建出這個被打印的對象。因此這里使用了類似調用對象構造函數的表達形式。
__repr__和__str__的區別在于,后者在str()函數被使用時調用,或者通過print函數隱式的調用。
如果你只想實現這兩個方法中的一個,那么__repr__是更好的選擇。因為當沒有自定義的__str__,__str__方法默認會調用__repr__。
算術運算符
上面的代碼中實現了兩個運算:+和*,用到了__add__和__mul__`。注意到這兩者情況中,每次運算都返回一個新的實例。這是中綴運算符的基本原則:創建新對象而不修改操作數(不可變類的思想)。
自定義的布爾值
默認情況下,自定義的類實例都會認為是true,除非實現了__bool__或__len__。bool(x)會調用x.__bool__(),如果__bool__沒有實現,那么Python會試著調用x.__len__(),如果該方法返回0,則bool(x)返回False,否則返回True。
我們基于這個概念來實現__bool__:如果向量的模是0,則返回False;否則返回True。
一種更高效的實現是:
但是可讀性不好。
集合API
下圖展示了比較重要的集合。該圖中所有的類都是ABCs——抽象基類(abstract base classes)。
該圖只是給大家看一下最重要的集合類實現了哪些特殊方法。
最上面的幾個ABCs有一個單獨的特殊方法,Python3.6中新增的Collection聯合了這三個重要的接口(Iterable、Sized和Container),這是每個集合類都應該實現的:
- Iterable支持迭代
- Sized支持內建的len函數
- Container支持in運算符
Python不需要類去真正繼承這些ABCs,而是,比如任何實現了__len__的類都滿足Sized接口。
三個非常重要的特殊集合是:
- Sequence,內置接口的形式化,如list和str
- Mapping,通過dict和collection.defaultdict實現
- Set,內置的set和frozenset的接口
其中只有Sequence是Reversible,可以逆序訪問的。
Set還實現了一個中綴運算&,比如a & b代表兩個集合的交集,通過__and__實現。
特殊方法一覽
下表展示了一些與運算符(中綴運算或abs等)無關的特殊方法。
| 字符串/字節序列表示 | __repr__,__format__,__bytes__,__fspath__ |
| 轉換成數值 | __abs__,__bool__,__complex__,__float__,__hash__,__index__ |
| 集合模擬 | __len__,__getitem__,__setitem__,__delitem__,__contains__ |
| 迭代 | __iter__,__aiter__,__next__,__anext__,__reversed__ |
| 可調用或協同執行 | __call__,__await__ |
| 上下文管理 | __enter__,__await__ |
| 實例創建和銷毀 | __new__,__init__,__del__ |
| 屬性管理 | __getattr__,__getattribute__,__setattr__,__delattr__,__dir__ |
| 屬性描述符 | __get__,__set__,__delete__,__set_name__ |
| 類服務 | __prepare__,__init_subclass__,__instancecheck__,__subclasscheck__ |
跟運算符相關(中綴和數值運算符)的特殊方法如下表所示:
| 一元數值運算 | __neg__ -,__pos__ +,__abs__ abs() |
| 眾多比較運算符 | __lt__ <,__le__ <=,__eq__ ==,__ne__ !=,__gt__ >,__ge__ >= |
| 算術運算符 | __add__ +,__sub__ -,__mul__ *,__truediv__ /,__floordiv__ //,__mod__ %,__divmod__ divmod(),__pow__ **或pow(),__round__ round(),__matmul__ @ |
| 反向算術運算符 | __radd__,__rsub__,__rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__, __rmatmul__ |
| 增量賦值算術運算符 | __iadd__, __isub__, __imul__, __itruediv__, __ifloordiv__, __imod__, __ipow__, __imatmul__ |
| 位運算符 | __invert__ ~,__lshift__ <<,__rshift__ >>, __and__ &, __or__ |,__xor__ ^ |
| 反向位運算符 | __rlshift__,__rrshift__, __rand__, __rxor__, __ror__ |
| 增量賦值位運算符 | __ilshift__, __irshift__, __iand__, __ixor__, __ior__ |
當交換兩個操作數的位置時,就會調用反向運算符(b * a而不是a * b)。增量運算符是一種把中綴運算符變成賦值運算的捷徑(a = a * b變成了a *= b)。
為什么len不是普通方法
如果x是一個內置類型的實例,那么len(x)的速度會非常快。背后的原理是CPython會直接從一個C結構體里讀取對象的長度,完全不會調用任何方法。
總結
通過實現特殊方法,能使你的對象像內建類型一樣。
通過實現__reper和__str__,Python對象能通過字符串的形式表示自己,前者用在調試和打印上,后者用于給使用對象的用戶。
總結
以上是生活随笔為你收集整理的《流畅的Python第二版》读书笔记——Python数据模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Alpha冲刺(7/10)
- 下一篇: 今天用python的turtle简单画了