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