《流暢的python》學習筆記
文章目錄
- 《流暢的python》學習筆記
-
- 寫在前面
- 1. Python資料模型
-
- 1.1 特殊方法
- 2. 序列構成的數組
-
- 2.1 内置序列類型
- 2.2 清單推導和生成器表達式
- 2.3 元組不僅僅是不可變的清單
- 2.4 切片
- 2.5 增量指派
- 2.6 排序
- 2.7 數組、記憶體視圖、NumPy和隊列
- 3. 字典和集合
-
- 3.1 泛映射類型和字典推導
- 3.2 常見的映射方法
- 3.3 映射的彈性鍵查詢
- 3.4 字典的變種
- 3.5 集合
- 3.6 dict和set的背後
- 4. 文本和位元組序列
-
- 4.1 字元和位元組
- 4.2 編碼和解碼
- 4.3 處理文本檔案
- 4.4 規範化Unicode字元串
- 4.5 支援字元串和位元組序列的雙模式API
- 5. 一等函數
-
- 5.1 把函數視作對象
- 5.2 函數的參數和注解
- 5.3 支援函數式程式設計的包
- 6. 使用一等函數實作涉及模式
- 7. 函數裝飾器和閉包
-
- 7.1 裝飾器基礎
- 7.2 閉包
- 7.3 裝飾器
- 8. 對象引用、可變性和垃圾回收
-
- 8.1 變量不是盒子
- 8.2 函數的參數作為引用
- 8.3 del、垃圾回收和弱引用
- 9. 符合Python風格的對象
-
- 9.1 對象的表示形式
- 9.2 格式化顯示
- 9.3 Python的私有屬性和受保護的屬性
- 10. 序列的修改、散列和切片
- 11. 接口:從協定到抽象基類
- 12. 繼承的優缺點
- 13. 正确重載運算符
- 14. 可疊代的對象、疊代器和生成器
- 15. 上下文管理器和else塊
- 16. 協程
- 17. 使用期物處理并發
- 18. 使用asyncio包處理并發
- 19. 動态屬性和特性
- 20. 屬性描述符
- 21. 類元程式設計
寫在前面
- 讀後感 優點:
- 翻譯滿昏!絕對滿昏!💯,你看下面黃色部分,這翻譯絕了,感覺我才是文化沙漠,什麼“親者快,仇者痛”,我這輩子沒見過這麼進階的用法。
-
我被作者舉的例子驚到了(見2.4),真的很有水準,翻譯和作者本身都很厲害
比如下面這一句
這兩行代碼的執行結果:t = (1, 2, [30, 40]) t[2] += [50, 60]
- 抛出異常:
因為 tuple 不支援對它的元素指派,是以會抛出 TypeError 異常。
-
的值發生了修改:t[2]
t = (1, 2, [30, 40, 50, 60])
- 抛出異常:
- 在講排序的時候,講到
和sorted
背後使用的排序算法是Timsort,這個算法的作者是Tim Peterslist.sort
- 這個算法的相關代碼在Google對Sun的侵權案中,當作了呈堂證供。
- 這個算法的作者也是
,python之禅的作者。import this
- 我靠,太離譜了,世界線收束,雞皮疙瘩都起來了。
- 每一章的小結真的寫的太好了,友善回顧這一章講了啥,也友善自己查漏補缺。
- 延伸閱讀也是驚豔啊,作者很明顯博覽群書,基礎紮實。
- 作者吹了一波《Python Cookbook(第三版)》和《Python Cookbook(第二版)》,我準備去學習學習!
- 作者每章的小結寫的很不錯,每次因為知識點需要複查書籍的時候,可以先看對應章節的 本章小結 ,再查。
- 讀後感 缺點:
-
讀到11章和12章的時候,就開始有點吃力了,不知道是不是我的知識儲備不夠。一些抽象的知識點作者和翻譯都有點力不從心(作為讀者的角度),就是好像作者想把這個點用比喻的方式說清楚,但是又很難把他心中的了解表達出來,甚至很多地方都是直接進行教條化的描述,對于翻譯來說,就更困難了。
【對不起,我面向對象學的太差了嗚嗚嗚】
比如 P540 中:優先使用對象組合,而不是類繼承,還有,組合和委托可以代替混入,把行為提供給不同的類,但是不能取代接口繼承去定義類型層次結構。
對于 組合、委托、混入、繼承等名詞的解釋不夠到位,這幾個名詞,我就對繼承還可以有深入的了解,其他的三個名詞出現的時候,一臉懵逼
- 從16章協程開始,我就開始絕望了起來,有點整不明白,咬牙硬吃到18章asyncio的一些知識點的時候,就是懵懵懂懂的,在 yield 和 yield from 中學傻了。在18.5章的時候,不知道為什麼突然出現了個semaphore,十分突兀,就開始不知所雲了起來。
-
- 傳送門:
- 清單、元組、數組、雙向隊列的方法和屬性
-
、dict
和collections.defaultdict
的方法清單collections.OrderedDict
- 集合的數學運算、集合的比較運算符、集合類型的其他方法
- 使用者定義函數的屬性
- 利用
提取函數簽名inspect.signature
-
等幾個方法差別小記__set__,__setattr__,__getattr__
- 屬性、特性、描述符
1. Python資料模型
-
:用來建構隻有少數屬性但是沒有方法的對象,比如資料庫條目。collections.nametuple
-
:方法可以實作切片效果__getitem__
def __getitem__(self, position): return self._cards[position]
1.1 特殊方法
- 如何使用特殊方法
- 首先,特殊方法的存在是為了被python解析器調用的,而不是被我們調用的
- 其次,
這種寫法應該修正為my_object.__len__()
,在執行len(my_object)
的時候,如果my_object是一個自定義類的對象,那麼python會自己去調用其中的,由我們自己實作的len(my_object)
方法__len__
- 如果是python内置的類型,如清單list、字元串str、位元組序列bytearray等,Cpython會抄近道,
實際上會直接傳回PyVarObject裡的ob_size屬性。其中__len__
PyVarObject
表示記憶體中長度可變的内置對象的C語言結構體。
直接讀取這個值比調用一個方法要快很多。
- 很多時候,特殊方法的調用是隐式的,比如
這個語句,背後其實調用的是for i in x
,而這個函數的背後則是iter(x)
方法。(前提是這個方法在x中被實作了)x.__iter__()
- 不要想當然的随意添加特殊方法,說不定以後python會用到這個名字。
-
:能把一個對象用字元串的形式表達出來以便辨認,「字元串表示形式」。repr
所傳回的字元串應該準确、無歧義,并且盡可能表達出如何用代碼建立出這個被列印的對象。__repr__
和__repr__
的差別在于,後者是在__str__
函數被使用,或者是在用str()
print
函數列印一個對象的時候才能被調用的,并且它傳回的字元串對終端使用者更友好。
如果你隻想實作這兩個特殊方法中的一個,
是更好的選擇,因為如果一個對象沒有__repr__
函數,而 Python 又需要調用它的時候,解釋器會用__str__
作為替代。__repr__
-
的背後是調用bool(x)
的結果;如果不存在x.__bool__()
方法,那麼__bool__
會嘗試調用bool(x)
。若傳回 0,則 bool 會傳回 False;否則傳回True。x.__len__()
- 跟運算符無關的特殊方法
- 字元串/位元組序清單示形式:
、__repr__
、__str__
、__format__
__bytes__
- 數值轉換:
、__abs__
、__bool__
、__complex__
、__int__
、__float__
、__hash__
__index__
- 集合模拟:
、__len__
、__getitem__
、__setitem__
、__delitem__
__contains__
- 疊代枚舉:
、__iter__
、__reversed__
__next__
- 可調用模拟:
__call__
- 上下文管理器:
、__enter__
__exit__
- 執行個體建立和銷毀:
、__new__
、__init__
__del__
- 屬性管理:
、__getattr__
、__getattribute__
、__setattr__
、__delattr__
__dir__
- 屬性描述符:
、__get__
、__set__
__delete__
- 跟類相關的服務:
、__prepare__
、__instancecheck__
__subclasscheck__
- 字元串/位元組序清單示形式:
- 跟運算符相關的特殊方pos法
- 一進制運算符:
(__neg__
)、-
(__pos__
)、+
(__abs__
)abs()
- 衆多比較運算符:
(__lt__
)、<
(__le__
)、<=
(__eq__
)、==
(__ne__
)、!=
(__gt__
)、>
(__ge__
)>=
- 算數運算符:
(__add__
)、+
(__sub__
)、-
(__mul__
)、*
(__truediv__
)、/
(__floordiv__
)、//
(__mod__
)、%
(__divmod__
)、divmod()
(__pow__
或**
)、pow()
(__round__
)round()
- 反向算數運算符:
、__radd__
、__rsub__
、__rmul__
、__rtruediv__
、__rfloordiv__
、__rmod__
__rdivmod__
- 增量指派算數運算符:
、__iadd__
、__isub__
、__imul__
、__itruediv__
、__ifloordiv__
、__imod__
__ipow__
- 位運算符:
(__invert__
)、~
(__lshift__
)、<<
(__rshift__
)、>>
(__and__
)、&
(__or__
)、|
(__xor__
)^
- 反向位運算符:
、__rlshift__
、__rrshift__
、__rand__
、__rxor__
__ror__
- 增量指派位運算符:
、__ilshift__
、__irshift__
、__iand__
、__ixor__
__ior__
- 一進制運算符:
- 為什麼len不是一個普通方法:「實用勝于純粹」,如果 x 是一個内置類型的執行個體,那麼
的速度會非常快。背後的原因是 CPython 會直接從一個 C 結構體裡讀取對象的長度,完全不會調用任何方法。擷取一個集合中元素的數量是一個很常見的操作,在len(x)
等類型上,這個操作必須高效。str、list、memoryview
換句話說,len 之是以不是一個普通方法,是為了讓 Python 自帶的資料結構可以走後門,abs 也是同理。但是多虧了它是特殊方法,我們也可以把 len 用于自定義資料類型
2. 序列構成的數組
2.1 内置序列類型
- 容器序列:list、tuple、collections.deque。可以存放不同類型的資料。
- 扁平序列:str、bytes、bytearray、memoryview、array.array。隻能容納一種類型。
- 容器序列存放的是它們所包含的任意類型的對象的引用,而扁平序列裡存放的是值而不是引用。換句話說,扁平序列其實是一段連續的記憶體空間。由此可見扁平序列其實更加緊湊,但是它裡面隻能存放諸如字元、位元組和數值這種基礎類型。
- 可變序列:list、bytearray、array.array、collections.deque、memoryview
- 不可變序列:tuple、str、bytes
- 可變序列(MutableSequence)和不可變序列(Sequence)的差異
2.2 清單推導和生成器表達式
- Python 會忽略代碼裡 []、{} 和 () 中的換行,是以如果你的代碼裡有多行的清單、清單推導、生成器表達式、字典這一類的,可以省略不太好看的續行符 \。
- 清單推導不會再有變量洩漏的問題
x = 'ABC' dummy = [ord(x) for x in x] print(x, dummy) # ABC [65, 66, 67]
- 生成器表達式的文法跟清單推導差不多,隻不過把方括号換成圓括号而已。
2.3 元組不僅僅是不可變的清單
- 除了用作不可變的清單,它還可以用于沒有字段名的記錄。
- 元組其實是對資料的記錄:元組中的每個元素都存放了記錄中一個字段的資料,外加這個字段的位置。正是這個位置資訊給資料賦予了意義。
- 在元組拆包中使用
也可以幫助我們把注意力集中在元組的部分元素上。用*
來處理剩下的元素*
a, b, *rest = range(5) print(a, b, rest) # 0 1 [2, 3, 4]
- 在平行指派中,
字首隻能用在一個變量名前面,但是這個變量可以出現在指派表達式的任意位置*
a, *body, c, d = range(5) print(a, body, c, d) # 0 [1, 2] 3 4
-
:建立一個具名元組需要兩個參數,一個是類名,另一個是類的各個字段的名字。後者可以是由數個字元串組成的可疊代對象,或者是由空格分隔開的字段名組成的字元串。collections.namedtuple
-
:一個包含這個類所有字段名稱的元組。_fields
-
:通過接受一個可疊代對象來生成這個類的一個執行個體,它的作用等價于_make()
是一樣的。類(*參數元組)
-
:把具名元組以_asdict()
的形式傳回,我們可以利用它來把元組裡的資訊友好地呈現出來。collections.OrderedDict
-
- 清單、元組、數組、雙向隊列的方法和屬性
方法和屬性 清單 元組 數組 雙向隊列 描述 s.__add__(s2)
√ √ √ ×
,拼接s+s2
s.__iadd__(s2)
√ × √ √
,就地拼接s+=s2
s.append(e)
√ × √ √ 在尾部添加一個新元素 s.appendleft(e)
× × × √ 添加一個元素到最左側(到第一個元素之前) s.byteswap
× × √ × 翻轉數組内每個元素的位元組序列,轉換位元組序列 s.clear()
√ × × √ 删除所有元素 s.__contains__(e)
√ √ √ × s是否包含e s.copy()
√ × × × 清單的淺複制 s.__copy__()
× × √ √ 對
(淺複制)的支援copy.copy
s.count(e)
√ √ √ √ e在s中出現的次數 s.__deepcopy__()
× × √ × 對
(深複制)的支援copy.deepcopy
s.__delitem__(p)
√ × √ √ 把位于p的元素删除 s.extend(it)
√ × √ √ 把可疊代對象it追加給s s.extendleft(i)
× × × √ 将可疊代對象i中的元素添加到頭部 s.frombytes(b)
× × √ × 将壓縮成機器值得位元組序列讀出來添加到尾部 s.fromfile(f,n)
× × √ × 将二進制檔案f内含有機器值讀出來添加到尾部,最多添加n項 s.fromlist(l)
× × √ × 将清單裡的元素添加到尾部,如果其中任何一個元素導緻了
異常,那麼所有的添加都會取消TypeError
s.__getitem__()
√ √ √ √
,擷取位置p的元素s[p]
s.__getnewargs__()
× √ × × 在pickle中支援更加優化的序列化 s.index(e)
√ √ √ × 在s中找到元素e第一次出現的位置 s.insert(p,e)
√ × √ × 在位置p之前插入元素e s.itemsize
× × √ × 數組中每個元素的長度是幾個位元組 s.__iter__()
√ √ √ √ 擷取s的疊代器 s.__len__()
√ √ √ √
,元素的數量len(s)
s.__mul__()
√ √ √ ×
,n個s的重複拼接s*n
s.__imul__()
√ × √ ×
,就地重複拼接s*=n
s.__rmul__()
√ √ √ ×
,反向拼接n*s
*
s.pop([p])
√ × √ √ 删除最後或者是(可選的)位于p的元素,并傳回它的值(注意,在雙向隊列中不接受參數) s.popleft()
× × × √ 移除第一個元素并傳回它的值 s.remove(e)
√ × √ √ 删除s中第一次出現的e s.reverse()
√ × √ √ 就地把s的元素倒序排列 s.__reversed__
√ × × √ 傳回s的倒序疊代器 s.rotate(n)
× × × √ 把n個元素從隊列的一段移到另一端 s.__setitem__(p,e)
√ × √ √
,把元素e放在位置p,替代已經在那個位置的元素s[p]=e
s.sort([key],[reverse])
√ × × × 就地對s中的元素進行排序,可選的參數有鍵(key)和是否倒序(reverse) s.tobytes()
× × √ × 把所有元素的機器值用bytes對象的形式傳回 s.tofile(f)
× × √ × 把所有元素以機器值的形式寫入一個檔案 s.tolist()
× × √ × 把數組轉換成清單,清單裡的元素類型是數字對象 s.typecode
× × √ × 傳回隻有一個字元的字元串,代表數組元素在C語言中的類型
2.4 切片
-
為什麼切片和區間會忽略最後一個元素:
在切片和區間操作裡不包含區間範圍的最後一個元素是 Python 的風格,這個習慣符合 Python、C 和其他語言裡以 0 作為起始下标的傳統。這樣做帶來的好處如下。
- 當隻有最後一個位置資訊時,我們也可以快速看出切片和區間裡有幾個元素:
和range(3)
都傳回 3 個元素。my_list[:3]
- 當起止位置資訊都可見時,我們可以快速計算出切片和區間的長度,用後一個數減去第一個下标(stop - start)即可。
- 這樣做也讓我們可以利用任意一個下标來把序列分割成不重疊的兩部分,隻要寫成
和my_list[:x]
就可以了。my_list[x:]
- 當隻有最後一個位置資訊時,我們也可以快速看出切片和區間裡有幾個元素:
-
:對slice(a, b, c)
進行求值的時候,Python 會調用seq[start:stop:step]
。seq.__getitem__(slice(start, stop, step))
-
:使用方法slice(start, stop, step)
a = slice(6, 40) item[a]
- 多元切片:如果要得到
的值,Python 會調用a[i, j]
a.__getitem__((i, j))
-
:x[i, ...]
的縮寫x[i, :, :, :]
- 給切片指派:如果把切片放在指派語句的左邊,或把它作為
操作的對象,我們就可以對序列進行 嫁接 、 切除 或 就地修改 操作。del
- 如果指派的對象是一個切片,那麼指派語句的右側必須是個可疊代對象。
- 即便隻有單獨一個值,也要把它轉換成可疊代的序列。
2.5 增量指派
-
和+
的陷阱:如果要生成二維序列:*
- 不能:
[['_']*3]*3
- 而要:
[['_']*3 for i in range(3)]
- 不能:
-
a += b
- 如果a實作了
方法,就相當于調用了__iadd__
a.extend(b)
- 如果a沒有實作
的話,__iadd__
就跟a += b
一樣了。首先計算a = a + b
,得到一個新的對象,然後指派給a。a + b
- 也就是說,在這個表達式中,變量名會不會被關聯到新的對象,完全取決于這個類型有沒有實作
方法。__iadd__
- 如果a實作了
- 對不可變序列進行重複拼接操作的話,效率會很低,因為每次都有一個新對象,而解釋器需要把原來對象中的元素先指派到新的對象裡,然後再追加新的元素。
是一個例外,因為對字元串做str
實在是太普遍了,是以CPython對它做了優化,為str初始化記憶體的時候,程式會為它留出額外的可擴充空間,是以進行增量操作的時候,并不會涉及複制原有字元串到新位置這類操作。+=
2.6 排序
- Python中的排序算法:Timesort 是穩定的,意思是就算兩個元素比不出大小,在每次排序的結果裡他們的相對位置是固定的。
-
:就地排序清單,也就是說不會把原清單複制一份,傳回值為list.sort
None
-
:會建立一個清單作為傳回值sorted
-
參數能讓你對一個混有數字字元和數值的清單進行排序。key
l = [28, 14, '28', 5, '9', '1', 0, 6, '23', 19] sorted(l, key=int) # [0, '1', 5, 6, '9', 14, 19, '23', 28, '28']
-
和sorted
背後的排序算法是 Timsort,它是一種自适應算法,會根據原始資料的順序特點交替使用插入排序和歸并排序,以達到最佳效率。這樣的算法被證明是很有效的,因為來自真實世界的資料通常是有一定的順序特點的。list.sort
- 用
來管理 已排序 的序列:二分法bisect
-
函數其實是bisect
函數的别名,後者還有個姊妹函數叫bisect_right
bisect_left
-
傳回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素會被放置于它相等的元素的前面bisect_left
-
傳回的則是跟它相等的元素之後的位置bisect_right
-
- 利用
進行評價分組bisect
def grade(score, breakpoints=[60, 70, 80, 90], grades='FDCBA'): i = bisect.bisect(breakpoints, score) return grades[i] [grade(score) for score in [33, 99, 77, 70, 89, 90, 100]] # ['F', 'A', 'C', 'C', 'B', 'A', 'A']
-
把變量item插入到序列seq中,并能保持seq的升序順序。bisect.insort(seq, item)
2.7 數組、記憶體視圖、NumPy和隊列
- 如果我們需要一個隻包含數字的清單,那麼
比array.array
更高效。通過list
和array.tofile
進行檔案的儲存和讀取。array.fromfile
-
:是一個内置類,它能讓使用者在不複制内容的情況下操作同一個數組的不同切片。memoryview
-
的概念跟數組子產品類似,能用不同的方式讀寫同一塊記憶體資料,而且内容位元組不會随意移動。memoryview.cast
會把同一塊記憶體裡的内容打包成一個全新的memoryview.cast
對象給你。memoryview
-
:numpy
- 将一維數組轉化為二維:
array.shape=(x, y)
- 将數組轉置:
、array.T
array.transpose()
- 将一維數組轉化為二維:
-
類(雙向隊列)是一個線程安全、可以快速從兩端添加或者删除元素的資料類型。collections.deque
-
,dq = deque(range(10), maxlen=10)
是一個可選參數,帶别找個隊列可以容納的元素的數量。maxlen
-
:隊列的旋轉操作接受一個參數n,當n>0時,隊列的最右邊的n個元素會被移動到隊列的左邊。當n<0時,最左邊的n個元素會被移動到右邊。dq.rotate(n)
-
和append
都是原子操作,也就說是 deque 可以在多線程程式中安全地當作先進先出的棧使用,而使用者不需要擔心資源鎖的問題。popleft
- 其他隊列的實作:
-
提供了queue
、Queue
、LifoQueue
。在滿員的時候,這些類不會扔掉舊的元素來騰出位置。相反,如果隊列滿了,它就會被鎖住,直到另外的線程移除了某個元素而騰出了位置。這一特性讓這些類很适合用來控制活躍線程的數量。PriorityQueue
-
實作了自己的Queue,跟multiprocessing
相似,是涉及給程序間通信用的。queue.Queue
可以讓任務管理變得更友善。multiprocessing.JoinableQueue
-
裡面有asyncio
、Queue
、LifoQueue
、PriorityQueue
,這些類受到JoinableQueue
和queue
子產品的影響,但是為異步程式設計裡的任務管理提供了專門的便利。multiprocessing
-
沒有隊列類,而是提供了heapq
和heappush
方法,讓使用者可以把可變序列當作堆隊列或者優先隊列來使用。heappop
-
- 清單傾向于存放有通用特性的元素;元組則恰恰相反,經常用來存放不同類型的元素。
3. 字典和集合
3.1 泛映射類型和字典推導
-
子產品中有collections.abc
和Mapping
這兩個抽象基類,它們的作用是為 dict 和其他類似的類型定義形式接口MutableMapping
-
可散列的資料類型(hashable)
如果一個對象是可散列的,那麼在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實作
方法。另外可散列對象還要有__hash__()
__eq__()
方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那麼它們的散列值一定是一樣的。
簡單來說,如果一個對象是可散列的資料類型的話,那它應是不可變的。
- list等可變對象是不可散列的,因為随着資料的改變他們的哈希值會變化導緻進入錯誤的哈希表。
- 元組的話,隻有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的。
- 一般使用者自定義的類型的對象都是可散列的,散列值就是它們的
函數的傳回值,是以所有這些對象在比較的時候都是不相等的。如果一個對象實作了id()
方法,并且在方法中用到了這個對象的内部狀态的話,那麼隻有當所有這些内部狀态都是不可變的情況下,這個對象才是可散列的。__eq__()
- Python 裡所有的不可變類型都是可散列的,這個說法其實是不準确的,比如雖然元組本身是不可變序列,它裡面的元素可能是其他可變類型的引用。
- 字典推導:
3.2 常見的映射方法
dict
、
collections.defaultdict
和
collections.OrderedDict
的方法清單
方法 | dict | defaultdict | OrderedDIct | 描述 |
---|---|---|---|---|
| √ | √ | √ | 移除所有元素 |
| √ | √ | √ | 檢查k是否在d中 |
| √ | √ | √ | 淺複制 |
| × | √ | × | 用于支援 |
| × | √ | × | 在 函數中被調用的函數,用以給未找到的元素設定值 |
| √ | √ | √ | ,移除鍵位k的元素 |
| √ | √ | √ | 将疊代器it裡的元素設定為映射裡的鍵,如果initial參數,就把它作為這些鍵對應的值(預設是None) |
| √ | √ | √ | 傳回鍵k對應的值,如果字典裡沒有鍵k,則傳回None或者default |
| √ | √ | √ | 讓字典d能用 的形式傳回鍵k對應的值 |
| √ | √ | √ | 傳回d裡所有的鍵值對 |
| √ | √ | √ | 擷取鍵的疊代器 |
| √ | √ | √ | 擷取所有的鍵 |
| √ | √ | √ | 可以用 的形式得到字典裡鍵值對的數量 |
| × | √ | × | 當 找不到對應鍵的時候,這個方法會被調用 |
| × | × | √ | 把鍵位k的元素移動到最靠前或者最靠後的位置(last的預設值是True) |
| √ | √ | √ | 傳回鍵k所對應的值,然後移除這個鍵值對。如果沒有這個鍵,傳回None或者default |
| √ | √ | √ | 随機傳回一個鍵值對并從字典裡移除它 |
| × | × | √ | 傳回倒序的鍵的疊代器 |
| √ | √ | √ | 若字典裡有鍵k,則把它對應的值設定位default,然後傳回這個值;若無,則讓 ,然後傳回default |
| √ | √ | √ | 實作 操作,把k對應的值設為v |
| √ | √ | √ | m可以是映射或者鍵值對疊代器,用來更新d裡對應的條目 |
| √ | √ | √ | 傳回字典裡的所有值 |
- 用setdefault處理找不到的鍵
- 使用
來代替d.get(k, default)
,可以防止報錯d[k]
- 字典處理優化
- 好的方法
- 差的方法
if key not in my_dict: my_dict[key] = [] my_dict[key].append(new_value)
- 二者的效果是一樣的,隻不過後者至少要進行兩次鍵查詢——如果鍵不存在的話,就是三次,用
隻需要一次就可以完成整個操作。setdefault
- 使用
3.3 映射的彈性鍵查詢
- 場景:有時候為了友善起見,就算某個鍵在映射裡不存在,我們也希望在通過這個鍵讀取值的時候能得到一個預設值。
- 方法:
-
類collections.defaultdict
- 把list構造方法作為
來建立一個default_factory
defaultdict
- 如果在建立
的時候沒有指定defaultdict
,查詢不存在的鍵會觸發KeyErrordefault_factory
-
裡的defaultdict
隻會在default_factory
裡被調用,在其他的方法裡完全不會發揮作用。比如__getitem__
會建立預設值并傳回該預設值,dd[k]
就會傳回Nonedd.get(k)
- 所有這一切的背後是基于特殊方法
實作的,它會在__missing__
遇到找不到的鍵的時候調用defaultdict
,而實際上這個特性是所有映射類型都可以選擇去支援的default_factory
"""建立一個從單詞到其出現情況的映射""" import sys import re from collections import defaultdict WORD_RE = re.compile(r'\w+') # index = {} index = defaultdict(list) with open(sys.argv[1], encoding='utf-8') as fp: for line_no, line in enumerate(fp, 1): for match in WORD_RE.finditer(line): word = match.group() column_no = match.start() + 1 location = (line_no, column_no) ''' 這其實是一種很不好的實作,這樣寫隻是為了證明論點 occurrences = index.get(word, []) occurrences.append(location) index[word] = occurrences ''' ''' 下面是好的實作,用到了setdefault函數 index.setdefault(word, []).append(location) ''' ''' 通過collections.defaultdict實作,取得key沒有的時候,建立這個key,并賦予預設值''' index[word].append(location) # 以字母順序列印出結果 for word in sorted(index, key=str.upper): print(word, index[word])
- 把list構造方法作為
- 自定義一個
子類,然後在子類中實作dict
方法__missing__
- 像
這種操作在Python3中是很快的,而且即便映射類型對象很龐大也沒關系。這是因為k in my_dict.keys()
的傳回值是一個視圖。dict.keys()
- 視圖就像一個集合,而且跟字典類似的是,在視圖裡查找一個元素的速度很快。
class StrKeyDict0(dict): def __missing__(self, key): if isinstance(key, str): raise KeyError(key) return self[str(key)] def get(self, key, default=None): try: return self[key] except KeyError: return default def __contains__(self, key): return key in self.keys() or str(key) in self.keys()
- 像
-
3.4 字典的變種
-
:這個類型在添加鍵的時候會保持順序,是以鍵的疊代次序總是一緻的。collections.OrderedDict
的OrderedDict
方法預設删除并傳回的是字典裡的最後一個元素,但是如果像popitem
這樣調用它,那麼它删除并傳回第一個被添加進去的元素。my_odict.popitem(last=False)
-
:該類型可以容納數個不同的映射對象,然後在進行鍵查找操作的時候,這些對象會被當作一個整體被逐個查找,直到鍵被找到為止。這個功能在給有嵌套作用域的語言做解釋器的時候很有用,可以用一個映射對象來代表一個作用域的上下文。collections.ChainMap
-
:這個映射類型會給鍵準備一個整數計數器。每次更新一個鍵的時候都會增加這個計數器。是以這個類型可以用來給可散清單對象計數,或者是當成多重集來用——多重集合就是集合裡的元素可以出現不止一次。collections.Counter
-
:統計字典中出現次數最多的資料my_dict.most_common(n)
ct = Counter({'a': 10, 'b': 2, 'r': 2, 'c': 1, 'd': 1, 'z': 3}) ct.most_common(2) # [('a', 10), ('z', 3)]
-
:這個類其實就是把标準colllections.UserDict
用純 Python 又實作了一遍。跟dict
、OrderedDict
和ChainMap
這些開箱即用的類型不同,Counter
是讓使用者繼承寫子類的。UserDict
- 更傾向于從
而不是從UserDict
繼承的主要原因是,後者有時會在某些方法的實作上走一些捷徑,導緻我們不得不在它的子類中重寫這些方法,但是 UserDict 就不會帶來這些問題。dict
- 更傾向于從
- 不可變映射類型:
。如果給這個類一個映射,它會傳回一個隻讀的映射視圖。雖然是個隻讀視圖,但是它是動态的。這意味着如果對原映射做出了改動,我們通過這個視圖可以觀察到,但是無法通過這個視圖對原映射做出修改。types.MappingProxyType
3.5 集合
- 集合可以去重
- 集合中的元素必須是可散列的,set 類型本身是不可散列的,但是frozenset 可以。是以可以建立一個包含不同 frozenset 的 set。
- 求交集:
,&
intersection
- 空集:
,而不是set()
,因為{}
是一個空字典{}
- 除了空集,集合的字元串表示形式總是以
的形式出現。{...}
-
的速度快于{1, 2, 3}
,因為後者的話,Python必須先從set這個名字來查詢構造方法,然後建立一個清單,最後再把這個清單傳入到構造方法裡。但是如果是像set([1, 2, 3])
這樣的字面量,Python 會利用一個專門的叫作 BUILD_SET 的位元組碼來建立集合。{1, 2, 3
- MutableSet和它的超類的UML類圖
- 集合的數學運算:這些方法或者會生成新集合,或者會在條件允許的情況下就地修改集合
數學符号 Python運算符 方法 描述 S ∩ Z S \cap Z S∩Z s&z
s.__and__(z)
s和z的交集 S ∩ Z S \cap Z S∩Z z&s
s.__rand__(z)
反向&操作 S ∩ Z S \cap Z S∩Z z&s
s.intersection(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求它們與s的交集 S ∩ Z S \cap Z S∩Z s&=z
s.__iand__(z)
把s更新為s和z的交集 S ∩ Z S \cap Z S∩Z s&=z
s.intersection_update(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求得它們與s的交集,然後把s更新成這個交集 S ∪ Z S \cup Z S∪Z s|z
s.__or__(z)
s和z的并集 S ∪ Z S \cup Z S∪Z z|s
s.__ror__(z)
|的反向操作 S ∪ Z S \cup Z S∪Z z|s
s.union(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求它們和s的并集 S ∪ Z S \cup Z S∪Z s|=z
s.__ior__(z)
把s更新為s和z的并集 S ∪ Z S \cup Z S∪Z s|=z
s.update(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求它們和s的并集,并把s更新成這個并集 S ∖ Z S \setminus Z S∖Z s-z
s.__sub__(z)
s和z的差集,或者叫作相對補集 S ∖ Z S \setminus Z S∖Z z-s
s.__rsub__(z)
-的反向操作 S ∖ Z S \setminus Z S∖Z z-s
s.difference(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求它們和s的差集 S ∖ Z S \setminus Z S∖Z s-=z
s.__isub__(z)
把s更新為它與z的差集 S ∖ Z S \setminus Z S∖Z s-=z
s.difference_update(it, ...)
把可疊代的it和其他所有參數轉化為集合,求它們和s的差集,然後把s更新成這個差集 S ∖ Z S \setminus Z S∖Z s-=z
s.symmetric_difference(it)
求s和set(it)的對稱差集 S ⨁ Z S \bigoplus Z S⨁Z s^z
s.__xor__(z)
求s和z的對稱差集 S ⨁ Z S \bigoplus Z S⨁Z z^s
s.__rxor__(z)
^的反向操作 S ⨁ Z S \bigoplus Z S⨁Z z^s
s.symmetric_difference_update(it, ...)
把可疊代的it和其他所有參數轉化為集合,然後求它們和s的對稱差集,最後把s更新成該結果 S ⨁ Z S \bigoplus Z S⨁Z s^=z
s.__ixor__(z)
把s更新成它與z的對稱差集 - 集合的比較運算符,傳回值是布爾類型
數學符号 Python運算符 方法 描述 s.isdisjoint(z)
檢視s和z是否不相交(沒有共同元素) e ∈ S e \in S e∈S e in s
s.__contains__(e)
元素e是否屬于s S ⊆ Z S \subseteq Z S⊆Z s <= z
s.__le__(z)
s是否為z的子集 S ⊆ Z S \subseteq Z S⊆Z s <= z
s.issubset(it)
把可疊代的it轉化為集合,然後檢視s是否為它的子集 S ⊂ Z S \subset Z S⊂Z s < z
s.__lt__(z)
s是否為z的真子集 S ⊇ Z S \supseteq Z S⊇Z s >= z
s.__ge__(z)
s是否為z的父集 S ⊇ Z S \supseteq Z S⊇Z s >= z
s.issuperset(it)
把可疊代的it轉化為集合,然後檢視s是否為它的父集 S ⊃ Z S \supset Z S⊃Z s > z
s.__gt__(z)
s是否為z的真父集 - 集合類型的其他方法
方法 set frozenset 描述 s.add(e)
√ × 把元素e添加到s中 s.clear()
√ × 移除掉s中的所有元素 s.copy()
√ √ 對s淺複制 s.discard(e)
√ × 如果s裡有e這個元素的話,把它移除 s.__iter__()
√ √ 傳回s的疊代器 s.__len__()
√ √ len(s) s.pop()
√ × 從s中移除一個元素并傳回它的值,若s為空,則抛出KeyError異常 s.remove(e)
√ × 從s中移除e元素,若e元素不存在,則抛出KeyError異常
3.6 dict和set的背後
- 如果兩個對象在比較的時候相等,那麼它們的散列值必須相等。
- 散列函數用于将鍵值經過處理後轉化為散列值。具有以下特性:
- 散列函數計算得到的散列值是非負整數
- 如果 key1 == key2,則 hash(key1) == hash(key2)
- 如果 key1 != key2,則 hash(key1) != hash(key2)
- 散列沖突:簡單來說,指的是 key1 != key2 的情況下,通過散列函數處理,hash(key1) == hash(key2),這個時候,我們說發生了散列沖突。設計再好的散列函數也無法避免散列沖突,原因是散列值是非負整數,總量是有限的,但是現實世界中要處理的鍵值是無限的,将無限的資料映射到有限的集合,肯定避免不了沖突。
- 從字典中取值的算法流程圖
- 用元組取代字典就能節省空間的原因有兩個:
- 是避免了散清單所耗費的空間
- 無需把記錄中字段的名字在每個元素裡都存一遍
- 在使用者自定義的類型中,
屬性可以改變執行個體屬性的存儲方式,由dict變成tuple__slots__
- 使用散清單給dict帶來的優勢和限制都有哪些
- 鍵必須是可散列的
- 字典在記憶體上的開銷巨大
- 鍵查詢很快
- 鍵的次序取決于添加順序
-
往字典裡添加新鍵可能會改變已有鍵的順序
無論何時往字典裡添加新的鍵,Python 解釋器都可能做出為字典擴容的決定。擴容導緻的結果就是要建立一個更大的散清單,并把字典裡已有的元素添加到新表裡。這個過程中可能會發生新的散列沖突,導緻新散清單中鍵的次序變化。
- 集合的特點:
- 集合裡的元素必須是可散列的
- 集合很消耗記憶體
- 可以很高效地判斷元素是否存在于某個集合
- 元素的次序取決于被添加到集合裡的次序
- 往集合裡添加元素,可能會改變集合裡已有元素的次序
4. 文本和位元組序列
4.1 字元和位元組
- 把碼位轉換成位元組序列的過程是編碼;把位元組序列轉換成碼位的過程是解碼。
- 把位元組序列變成人類可讀的文本字元串就是解碼(
),而把字元串變成用于存儲或傳輸的位元組序列就是編碼(decode
)。encode
-
對象可以從bytes
對象使用給定的編碼構造,各個元素是str
内的整數。range(256)
對象的切片還是bytes
對象,即使是隻有一個字元的切片。bytes
-
對象沒有字面量句法,而是以bytearray
和位元組序列字面量參數的形式顯示。bytearray()
對象的切片還是bytearray
對象。bytearray
- 這裡比較特殊,因為
擷取的是一個整數,而my_bytes[0]
傳回的是一個長度為1的my_bytes[:1]
對象。bytes
- 雖然二進制序列其實是整數序列,但是它們的字面量表示法表明其中有ASCII 文本。是以,各個位元組的值可能會使用下列三種不同的方式顯示。
- 可列印的 ASCII 範圍内的位元組(從空格到 ~),使用 ASCII 字元本身。
- 制表符、換行符、回車符和
對應的位元組,使用轉義序列\
、\t
、\n
和\r
。\\
- 其他位元組的值,使用十六進制轉義序列(例如,
是空位元組)。\x00
- 二進制序列有個類方法是
沒有的,名為str
,它的作用是解析十六進制數字對(數字對之間的空格是可選的),建構二進制序列:fromhex
bytes.fromhex('31 4B CE A9') # b'1K\xce\xa9'
- 使用緩沖類對象建構二進制序列是一種低層操作,可能涉及類型轉換:
import array
numbers = array.array('h', [-2, -1, 0, 1, 2])
octets = bytes(numbers)
# b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
- 使用緩沖類對象建立
或bytes
對象時,始終複制源對象中的位元組序列。與之相反,bytearray
對象允許在二進制資料結構之間共享記憶體。memoryview
-
對象的切片是一個新memoryview
memoryview
對象,而且不會
複制位元組序列
- 如果使用
子產品把圖像打開為記憶體映射檔案,那麼會複制少量位元組mmap
4.2 編碼和解碼
- 某些編碼(如
和多位元組的ASCII
)不能表示所有GB2312
字元Unicode
- UTF 編碼的設計目的就是處理每一個
碼位Unicode
- 典型編碼:
- latin1(即 iso8859_1):一種重要的編碼,是其他編碼的基礎,例如 cp1252 和Unicode(注意,latin1 與 cp1252 的位元組值是一樣的,甚至連碼位也相同)。
- cp1252:Microsoft 制定的 latin1 超集,添加了有用的符号,例如彎引号和€(歐元);有些 Windows 應用把它稱為“ANSI”,但它并不是 ANSI 标準。
- cp437:IBM PC 最初的字元集,包含框圖符号。與後來出現的 latin1 不相容。
- gb2312:用于編碼簡體中文的陳舊标準;這是亞洲語言中使用較廣泛的多位元組編碼之一。
- utf-8:目前 Web 中最常見的 8 位編碼;與 ASCII 相容(純 ASCII 文本是有效的 UTF-8 文本)。
- utf-16le:UTF-16 的 16 位編碼方案的一種形式;所有 UTF-16 支援通過轉義序列(稱為“代理對”,surrogate pair)表示超過 U+FFFF 的碼位。
-
UnicodeEncodeError
:多數非 UTF 編解碼器隻能處理 Unicode 字元的一小部分子集。把文本轉換成位元組序列時,如果目标編碼中沒有定義某個字元,那就會抛出
UnicodeEncodeError 異常,除非把 errors 參數傳給編碼方法或函數,對錯誤進行特殊處理。
無法編碼時:
-
處理方式悄無聲息地跳過無法編碼的字元;這樣做通常很是不妥。error='ignore'
- 編碼時指定
,把無法編碼的字元替換成 ‘?’;資料損壞了,但是使用者知道出了問題。error='replace'
-
把無法編碼的字元替換成 XML 實體。error='xmlcharrefreplace'
- 編解碼器的錯誤處理方式是可擴充的。你可以為 errors 參數注冊額外的字元串,方法是把一個名稱和一個錯誤處理函數傳給
函數。codecs.register_error
-
-
:把二進制序列轉換成文本時,遇到無法轉換的位元組序列時會抛出UnicodeDecodeError;另一方面,很多陳舊的 8 位編碼——如 ‘cp1252’、‘iso8859_1’ 和’koi8_r’——能解碼任何位元組序列流而不抛出錯誤,例如随機噪聲。是以,如果程式使用錯誤的 8 位編碼,解碼過程悄無聲息,而得到的是無用輸出。UnicodeDecodeError
- 亂碼字元稱為鬼符(gremlin)或 mojibake(文字化け,“變形文本”的日文)。
- 使用預期之外的編碼加載子產品時抛出的SyntaxError
- Python 3 為所有平台設定的預設編碼都是 UTF-8
- Python 3 允許在源碼中使用非 ASCII 辨別符
-
:識别所支援的30中編碼。解決問題:如何找出位元組序列的編碼?Chardet
- 二進制序列編碼文本通常不會明确指明自己的編碼,但是 UTF 格式可以在文本内容的開頭添加一個位元組序标記。
- BOM:位元組序标記,byte-order-mark,指明編碼時使用 Intel CPU 的小位元組序
- 在小位元組序裝置中,各個碼位的最低有效位元組在前面:字母 ‘E’ 的碼位是 U+0045(十進制數 69),在位元組偏移的第 2 位和第 3 位編碼為 69 和0。
- 在大位元組序 CPU 中,編碼順序是相反的;‘E’ 編碼為 0 和 69。
-
為了避免混淆,UTF-16 編碼在要編碼的文本前面加上特殊的不可見字元 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小位元組序系統中,這個字元編碼為 b’\xff\xfe’(十進制數 255, 254)。因為按照設計,U+FFFE 字元不存在,在小位元組序編碼中,位元組序列 b’\xff\xfe’ 必定是 ZERO WIDTH NO-BREAK SPACE,是以編解碼器知道該用哪個位元組
序。
- UTF-16 有兩個變種:UTF-16LE,顯式指明使用小位元組序;UTF-16BE,顯式指明使用大位元組序。如果使用這兩個變種,不會生成 BOM。
- 與位元組序有關的問題隻對一個字(word)占多個位元組的編碼(如 UTF-16 和 UTF-32)有影響。UTF-8 的一大優勢是,不管裝置使用哪種位元組序,生成的位元組序列始終一緻,是以不需要 BOM。
-
盡管如此,某些Windows 應用(尤其是 Notepad)依然會在 UTF-8 編碼的檔案中添加
BOM;而且,Excel 會根據有沒有 BOM 确定檔案是不是 UTF-8 編碼,否則,它假設内容使用 Windows 代碼頁(codepage)編碼。UTF-8 編碼的 U+FEFF 字元是一個三位元組序列:
。是以,如果檔案以這三個位元組開頭,有可能是帶有 BOM 的 UTF-8 檔案。然而,Python 不會因為檔案以 b’\xef\xbb\xbf’ 開頭就自動假定它是 UTF-8編碼的。b'\xef\xbb\xbf'
4.3 處理文本檔案
- 處理文本的最佳實踐是 Unicode三明治 。
- 要盡早把輸入(例如讀取檔案時)的位元組序列解碼成字元串。
- 在程式的業務邏輯中隻能處理字元串對象。在其他處理過程中,一定不能編碼或解碼。
- 對輸出來說,則要盡量晚地把字元串編碼成位元組序列。
- 如果打開檔案是為了寫入,但是沒有指定編碼參數,會使用區域設定中的預設編碼,而且使用那個編碼也能正确讀取檔案。
- 如果腳本要生成檔案,而位元組的内容取決于平台或同一平台中的區域設定,那麼就可能導緻相容問題。
- 探索編碼預設值
輸出:import sys, locale expressions = """ locale.getpreferredencoding() type(my_file) my_file.encoding sys.stdout.isatty() sys.stdout.encoding sys.stdin.isatty() sys.stdin.encoding sys.stderr.isatty() sys.stderr.encoding sys.getdefaultencoding() sys.getfilesystemencoding() """ my_file = open('dummy', 'w') for expression in expressions.split(): value = eval(expression) print(expression.rjust(30), '->', repr(value))
locale.getpreferredencoding() -> 'cp936' type(my_file) -> <class '_io.TextIOWrapper'> my_file.encoding -> 'cp936' sys.stdout.isatty() -> False sys.stdout.encoding -> 'UTF-8' sys.stdin.isatty() -> False sys.stdin.encoding -> 'cp936' sys.stderr.isatty() -> False sys.stderr.encoding -> 'UTF-8' sys.getdefaultencoding() -> 'utf-8' sys.getfilesystemencoding() -> 'utf-8'
- 如果打開檔案時沒有指定
參數,預設值由encoding
提供locale.getpreferredencoding()
- 如果設定了
環境變量,PYTHONIOENCODING
的編碼使用設定的值;否則,繼承自所在的控制台;如果輸入/輸出重定向到檔案,則由sys.stdout/stdin/stderr
定義locale.getpreferredencoding()
- Python在二進制資料和字元串之間轉換時,内部使用
獲得的編碼;Python3很少如此,但仍有發生。這個設定不能修改。sys.getdefaultencoding()
-
用于編解碼檔案名(不是檔案内容)。把字元串參數作為檔案名傳給sys.getfilesystemencoding()
函數時就會使用它;如果傳入的檔案名參數是位元組序列,那就不經改動直接傳給 OS API。open()
4.4 規範化Unicode字元串
- 因為 Unicode 有組合字元(變音符号和附加到前一個字元上的記号,列印時作為一個整體),是以字元串比較起來很複雜。
s1 = 'café' s2 = 'cafe\u0301' s1, s2 len(s1), len(s2) s1 == s2 # ('café', 'café') # (4, 5) # False
- 在Unicode 标準中,‘é’ 和 ‘e\u0301’ 這樣的序列叫“标準等價物”(canonical equivalent),應用程式應該把它們視作相同的字元。但是,Python 看到的是不同的碼位序列,是以判定二者不相等。
- 這個問題的解決方案是使用
函數提供的Unicode 規範化。這個函數的第一個參數是這 4 個字元串中的一個:‘NFC’、‘NFD’、‘NFKC’ 和 ‘NFKD’。unicodedata.normalize
from unicodedata import normalize len(normalize('NFC', s1)), len(normalize('NFC', s3)) # (4, 4) len(normalize('NFD', s1)), len(normalize('NFD', s3)) # (5, 5) normalize('NFC', s1) == normalize('NFC', s2) # True normalize('NFD', s1) == normalize('NFD', s3) # True
- 西方鍵盤通常能輸出組合字元,是以使用者輸入的文本預設是 NFC 形式。不過,安全起見,儲存文本之前,最好使用
清洗字元串。normalize('NFC', user_text)
- NFC 也是 W3C 的“Character Model for the World Wide Web: String Matching and Searching”規範推薦的規範化形式。
- 使用 NFC 時,有些單字元會被規範成另一個單字元。這兩個字元在視覺上是一樣的,但是比較時并不相等,是以要規範化,防止出現意外。
-
在另外兩個規範化形式(NFKC 和 NFKD)的首字母縮略詞中,字母 K 表示 “compatibility”(相容性)。這兩種是較嚴格的規範化形式,對“相容字元”有影響。雖然 Unicode 的目标是為各個字元提供 “規範的” 碼位,但是為了相容現有的标準,有些字元會出現多次。
微符号是一個 “相容字元”。
- 使用 NFKC 和 NFKD 規範化形式時要小心,而且隻能在特殊情況中使用,例如搜尋和索引,而不能用于持久存儲,因為這兩種轉換會導緻資料損失。
- 大小寫折疊:
,就是把所有文本變成小寫,再做些其他轉換,與str.casefold()
基本一緻,但是存在例外:微符号 ‘μ’ 會變成小寫的希臘字母“μ”(在多數字型中二者看起來一樣);德語 Eszett(“sharp s”,ß)會變成“ss”str.lower()
- 去掉變音符号的優勢:
- 搜尋友善:人們有時很懶,或者不知道怎麼正确使用變音符号,而且拼寫規則會随時間變化,是以實際語言中的重音經常變來變去。
- URL可讀性:去掉變音符号還能讓 URL 更易于閱讀,至少對拉丁語系語言是如此。
- 變音符号對排序有影響的情況很少發生,隻有兩個詞之間唯有變音符号不同時才有影響。此時,帶有變音符号的詞排在正常詞的後面。
- 在 Python 中,非 ASCII 文本的标準排序方式是使用
函數,根據 locale 子產品的文檔,這個函數會“把字元串轉換成适合所在區域進行比較的形式”。locale.strxfrm
- PyUCA:Unicode 排序算法(Unicode Collation Algorithm,UCA)的純 Python 實作。PyUCA 沒有考慮區域設定。如果想定制排序方式,可以把自定義的排序表路徑傳給 Collator() 構造方法。PyUCA 預設使用項目自帶的
。allkeys.txt
import pyuca coll = pyuca.Collator() fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] sorted_fruits = sorted(fruits, key=coll.sort_key) sorted_fruits
-
Unicode 标準提供了一個完整的資料庫(許多格式化的文本檔案),不僅包括碼位與字元名稱之間的映射,還有各個字元的中繼資料,以及字元之間的關系。
Unicode 資料庫記錄了字元是否可以列印、是不是字母、是不是數字,或者是不是其他數值符号。unicodedata 子產品中有幾個函數用于擷取字元的中繼資料。例如,字元在标準中的官方名稱是不是組合字元(如結合波形符構成的變音符号等),以及符号對應的人類可讀數值(不是碼位)。
-
import unicodedata import re re_digit = re.compile(r'\d') sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' for char in sample: print( 'U+%04x' % ord(char), char.center(6), 're_dig' if re_digit.match(char) else '-', 'isdig' if char.isdigit() else '-', 'isnum' if char.isnumeric() else '-', format(unicodedata.numeric(char), '5.2f'), unicodedata.name(char), sep='\t' )
4.5 支援字元串和位元組序列的雙模式API
- 可以使用正規表達式搜尋字元串和位元組序列,但是在後一種情況中,ASCII 範圍外的位元組不會當成數字群組成單詞的字母。
- 字元串模式 r’\d+’ 能比對泰米爾數字和 ASCII 數字。
- 位元組序列模式 rb’\d+’ 隻能比對 ASCII 位元組中的數字。
- 字元串模式 r’\w+’ 能比對字母、上标、泰米爾數字和 ASCII 數字。
- 位元組序列模式 rb’\w+’ 隻能比對 ASCII 位元組中的字母和數字。
- 字元串正規表達式有個 re.ASCII 标志,它讓\w、\W、\b、\B、\d、\D、\s 和 \S 隻比對 ASCII 字元。
- 為了便于手動處理字元串或位元組序列形式的檔案名或路徑名,os 子產品提供了特殊的編碼和解碼函數。
-
:如果 filename 是 str 類型(此外還可能是 bytes 類型),使用fsencode(filename)
傳回的編解碼器把 filename 編碼成位元組序列;否則,傳回未經修改的 filename 位元組序列。sys.getfilesystemencoding()
-
:如果 filename 是 bytes 類型(此外還可能是 str 類型),使用fsdecode(filename)
傳回的編解碼器把 filename 解碼成字元串;否則,傳回未經修改的 filename 字元串。sys.getfilesystemencoding()
-
surrogateescape
:在 Unix 衍生平台中,這些函數使用 surrogateescape 錯誤處理方式以避免遇到意外位元組序列時卡住。Windows 使用的錯誤處理方式是 strict。
這種錯誤處理方式會把每個無法解碼的位元組替換成 Unicode 中 U+DC00 到 U+DCFF 之間的碼位(Unicode 标準把這些碼位稱為“Low Surrogate Area”),這些碼位是保留的,沒有配置設定字元,供應用程式内部使用。編碼時,這些碼位會轉換成被替換的位元組值。
- 在 Python 3.3 之前,編譯 CPython 時可以配置在記憶體中使用 16 位或 32 位存儲各個碼位。16 位是“窄建構”(narrow build),32 位是“寬建構”(wide build)。如果想知道用的是哪個,要檢視
的值:65535 表示“窄建構”,不能透明地處理U+FFFF 以上的碼位。“寬建構”沒有這個限制,但是消耗的記憶體更多:每個字元占 4 個位元組,就算是中文象形文字的碼位大多數也隻占 2 個位元組。這兩種建構沒有高下之分,應該根據自己的需求選擇。sys.maxunicode
- 靈活的字元串表述類似于 Python 3 對 int 類型的處理方式:如果一個整數在一個機器字中放得下,那就存儲在一個機器字中;否則解釋器切換成變長表述,類似于 Python 2 中的 long 類型。
-
5. 一等函數
- 在 Python 中,函數是一等對象。一等對象的定義:
- 在運作時建立
- 能指派給變量或資料結構中的元素
- 能作為參數傳給函數
- 能作為函數的傳回結果
- 人們經常将“把函數視作一等對象”簡稱為“一等函數”。這樣說并不完美,似乎表明這是函數中的特殊群體。在 Python 中,所有函數都是一等對象。
5.1 把函數視作對象
-
是函數對象衆多屬性中的一個,顯示函數的備注__doc__
-
:輸出的文本來自函數對象的help(f)
屬性__doc__
- 函數對象的 一等 本性:
- 把 factorial 函數指派給變量 fact,然後通過變量名調用
-
把它作為參數傳給map 函數,傳回一個可疊代對象,裡面的元素是把第一個參數
(一個函數)應用到第二個參數(一個可疊代對象)中各個元素上得到的結果
-
高階函數:higher-order function,接受函數為參數,或者把函數作為結果傳回的函數
這樣的函數例如:map,sorted,reduce,filter,apply(已移除)
map、filter 和 reduce 這三個高階函數還能見到,不過多數使用場景下都有更好的替代品。
- 清單推導 或 生成器表達式 具有map和filter兩個函數的功能,且更易于閱讀
list(map(fact, range(6))) [fact(n) for n in range(6)]
list(map(factorial, filter(lambda n: n % 2, range(6)))) [fact(n) for n in range(7) if n % 2 != 0]
-
reduce 是内置函數,最常用于求和,現在最好使用内置的 sum 函數,在可讀性和性能方面,這是一項重大改善
sum 和 reduce 的通用思想是把某個操作連續應用到序列的元素上,累計之前的結果,把一系列值歸約成一個值。
from functools import reduce from operator import add reduce(add, range(100)) sum(range(100))
- all 和 any 也是内置的歸約函數
-
:如果 iterable 的每個元素都是真值,傳回 True;all(iterable)
傳回True。all([])
-
:隻要 iterable 中有元素是真值,就傳回 True;any(iterable)
傳回False。any([])
-
- 清單推導 或 生成器表達式 具有map和filter兩個函數的功能,且更易于閱讀
-
匿名函數:lambda 關鍵字在 Python 表達式内建立匿名函數。
lambda 句法隻是文法糖:與 def 語句一樣,lambda 表達式會建立函數對象。這是 Python 中幾種可調用對象的一種。
- 調用類的時候會運作類的
方法建立一個執行個體,然後運作__new__
方法,初始化執行個體,最後把執行個體傳回給調用方。__init__
- 因為Python沒有new運算符,是以調用類相當于調用函數。
- 如果類定義了
方法,那麼它的執行個體可以作為函數調用。__call__
- 生成器函數:使用
關鍵字的函數或方法。調用生成器函數傳回的是生成器對象。(生成器函數還可以作為協程)yield
- Python 中有各種各樣可調用的類型,是以判斷對象能否調用,最安全的方法是使用内置的
函數callable()
[callable(obj) for obj in (abs, str, 13)] # [True, True, False]
- 任何 Python 對象都可以表現得像函數。為此,隻需實作執行個體方法
__call__
- 函數内省:除了
,函數對象還有其他屬性:__doc__
dir(factorial)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
- 與使用者定義的正常類一樣,函數使用
屬性存儲賦予它的使用者屬性。這相當于一種基本形式的注解。__dict__
- 列出 正常對象沒有 而 函數對象有 的屬性:
class C: pass obj = C() def func(): pass str(sorted(set(dir(func)) - set(dir(obj))))
['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
- 使用者定義函數的屬性:
名稱 類型 說明 __annotations__
dict 參數和傳回值的注解 __call__
method-wrapper 實作()運算符;即可調用對象協定 __closure__
tuple 函數閉包,即自由變量的綁定(通常是None) __code__
code 編譯成位元組碼的函數中繼資料和函數定義體 __defaults__
tuple 形式參數的預設值 __get__
method-wrapper 實作隻讀描述符協定 __globals__
dict 函數所在子產品中的全局變量 __kwdefaults__
dict 僅限關鍵字形式參數的預設值 __name__
str 函數名稱 __qualname__
str 函數的限定名稱,如 Random.choice
- 與使用者定義的正常類一樣,函數使用
5.2 函數的參數和注解
- 僅限關鍵字參數:keyword-only argument,調用函數時使用
和*
展開可疊代對象,映射到單個參數。**
- 在傳參的時候,将字典加上
作為參數傳遞,實作的是字典中所有元素作為單個參數傳入,同名的鍵回綁定到對應的具名參數上,餘下的則被**
捕獲。**attrs
-
僅限關鍵字參數:它一定不會捕獲未命名的定位參數。
此種參數隻能由關鍵字提供,絕對不會被位置參數自動填充。
- 定義函數時若想指定僅限關鍵字參數,要把它們放到前面有
的參數後面。*
-
如果不想支援數量不定的定位參數,但是想支援僅限關鍵字參數,在簽名中放
一個
*
- 允許正常參數出現在可變參數之後:此時這個正常參數就是一個僅限關鍵字參數。強制性的,它隻能通過關鍵字傳參
- 僅限關鍵字參數不需要有預設值, 由于Python需要将所有的參數都綁定一個值,而且将值綁定到關鍵字參數的唯一方法是通過這個關鍵字,是以這種參數是 需要關鍵字的參數 。是以這些參數必須通過調用方提供,且必須通過關鍵字提供值。
- 文法上的更改是允許省略可變參數的參數名。這意味着對于一個有僅限關鍵字參數的函數來說,它不會再接受一個可變參數。
- 定義函數時若想指定僅限關鍵字參數,要把它們放到前面有
- 擷取關于參數的資訊
-
使用HTTP微架構Bobo中有個使用函數内省的好例子。
啟動方式:
bobo -f hello.py
import bobo @bobo.query('/') def hello(person): return "Hello %s" % person
- 函數對象有個
屬性,它的值是一個元組,裡面儲存着定位參數和關鍵字參數的預設值。僅限關鍵字參數的預設值在__defaults__
屬性中。然而,參數的名稱在__kwdefaults__
屬性中,它的值是一個 code 對象引用,自身也有很多屬性。__code__
-
- 通過
提取函數的簽名inspect.signature
-
函數傳回一個inspect.signature
對象,它有一個inspect.Signature
屬性,這是一個有序映射,把參數名和parameters
對象對應起來。各個inspect.Parameter
屬性也有自己的屬性,例如Parameter
、name
和default
。特殊的kind
值表示沒有預設值,考慮到inspect._empty
是有效的預設值(也經常這麼做),而且這麼做是合理的。None
-
的屬性的值,為kind
類中的5個值之一:_ParameterKind
-
:可以通過定位參數和關鍵字參數傳入的形參(多數 Python 函數的參數屬于此類)。POSITIONAL_OR_KEYWORD
-
:定位參數元組。VAR_POSITIONAL
-
:關鍵字參數元組。VAR_KEYWORD
-
:僅限關鍵字參數(Python 3 新增)。KEYWORD_ONLY
-
:僅限定位參數;目前,Python 聲明函數的句法不支援,但是有些使用 C 語言實作且不接受關鍵字參數的函數(如 divmod)支援。POSITIONAL_ONLY
-
-
- 函數注解
- 函數聲明中的各個參數可以在 : 之後增加注解表達式。如果參數有預設值,注解放在參數名和 = 号之間。
- 如果想注解傳回值,在 ) 和函數聲明末尾的 : 之間添加 -> 和一個表達式。那個表達式可以是任何類型。
- 注解中最常用的類型是類(如 str 或 int)和字元串(如 ‘int > 0’)
- 注解不會做任何處理,隻是存儲在函數的
屬性中__annotations__
- Python 對注解所做的唯一的事情是,把它們存儲在函數的
屬性裡。僅此而已,Python 不做檢查、不做強制、不做驗證,什麼操作都不做。換句話說,注解對 Python 解釋器沒有任何意義。__annotations__
-
函數注解的最大影響或許不是讓 Bobo 等架構自動設定,而是為 IDE 和
lint 程式等工具中的靜态類型檢查功能提供額外的類型資訊。
5.3 支援函數式程式設計的包
- operator子產品
- 使用reduce函數和一個匿名函數計算階乘
def fact(n): return reduce(lambda a,b: a*b, range(1, n+1))
- operator 子產品為多個算術運算符提供了對應的函數,進而避免編寫
這種平凡的匿名函數lambda a, b: a*b
def fact2(n): return reduce(mul, range(1, n+1))
- 使用 itemgetter 排序一個元組清單
- itemgetter 使用
運算符,是以它不僅支援序列,還支援映射和任何實作[]
方法的類。__getitem__
-
的作用與itemgetter(1)
一樣lambda fields:fields[1]
metro_data = [ ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)), ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)), ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)), ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)), ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)), ] from operator import itemgetter for city in sorted(metro_data, key=itemgetter(2)): print(city)
- itemgetter 使用
- 如果把多個參數傳給 itemgetter,它建構的函數會傳回提取的值構成的元組:
cc_name = itemgetter(1, 0) for city in metro_data: print(cc_name(city))
- attrgetter 與 itemgetter 作用類似,它建立的函數根據名稱提取對象的屬性
from collections import namedtuple LatLong = namedtuple('LatLong', 'lat long') Metropolis = namedtuple('Metropolis', 'name cc pop coord') metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data] metro_areas metro_areas[0].coord.lat from operator import attrgetter name_lat = attrgetter('name', 'coord.lat') for city in sorted(metro_areas, key=attrgetter('coord.lat')): print(name_lat(city))
- attrgetter 與 itemgetter 個人總結:
- itemgetter:偏向于數組的切片操作
- attrgetter:偏向于類的屬性擷取操作
- methodcaller 的作用與 attrgetter 和 itemgetter 類似,它會自行建立函數,methodcaller 建立的函數會在對象上調用參數指定的方法:
from operator import methodcaller s = 'The tiem has come' upcase = methodcaller('upper') upcase(s) hiphenate = methodcaller('replace', ' ', '-') hiphenate(s)
- methodcaller還可以當機某些參數,也就是部分應用(partial application),這與
函數的作用類似。functoosl.partial
- 使用reduce函數和一個匿名函數計算階乘
- 使用
當機參數functools.partial
- functools.partial 這個高階函數用于部分應用一個函數。部分應用是指,基于一個函數建立一個新的可調用對象,把原函數的某些參數固定。使用這個函數可以把接受一個或多個參數的函數改編成需要回調的API,這樣參數更少。
from operator import mul from functools import partial triple = partial(mul, 3) triple(7) list(map(triple, range(1, 10)))
- 固定nfc函數,标準化字元串編碼處理
import unicodedata, functools nfc = functools.partial(unicodedata.normalize, 'NFC') s1 = 'café' s2 = 'cafe\u0301' s1, s2 s1 == s2 nfc(s1) == nfc(s2)
-
函數的作用與functools.partialmethod
一樣,不過是用于處理方法的。partial
- 對于類方法,partial就沒辦法了,是以新引用了
partialmethod
- 參考連結
- Python functools 子產品
- 對于類方法,partial就沒辦法了,是以新引用了
- functools 子產品中的 lru_cache 函數令人印象深刻,它會做備忘(memoization),這是一種自動優化措施,它會存儲耗時的函數調用結果,避免重新計算。
- functools.partial 這個高階函數用于部分應用一個函數。部分應用是指,基于一個函數建立一個新的可調用對象,把原函數的某些參數固定。使用這個函數可以把接受一個或多個參數的函數改編成需要回調的API,這樣參數更少。
6. 使用一等函數實作涉及模式
- 實作“政策” 模式:
- 經典“政策”模式
- 使用函數實作“政策”模式
- 政策對象通常是很好的享元:
- 享元:flyweight,享元是可共享的對象,可以同時再多個上下文中使用
- 可以避免運作時消耗
- 函數比使用者定義的類的執行個體輕量,而且無需使用“享元”模式,因為各個政策函數在 Python 編譯子產品時隻會建立一次。普通的函數也是“可共享的對象,可以同時在多個上下文中使用”。
- 在 Python 中,子產品也是一等對象,而且标準庫提供了幾個處理子產品的函數
-
globals()
:傳回一個字典,表示目前的全局符号表。這個符号表始終針對目前子產品(對函數或方法來說,是指定義它們的子產品,而不是調用它們的模
塊)。
-
- 指令模式
- 可以通過把函數作為參數傳遞而簡化。
- “指令”模式的目的是解耦調用操作的對象(調用者)和提供實作的對象(接收者)。
- 這個模式的做法是,在二者之間放一個 Command 對象,讓它實作隻有一個方法(execute)的接口,調用接收者中的方法執行所需的操作。這樣,調用者無需了解接收者的接口,而且不同的接收者可以适應不同的 Command 子類。調用者有一個具體的指令,通過調用 execute 方法執行。
class MacroCommand: """一個執行一組指令的指令""" def __init__(self, commands): self.commands = list(commands) def __call__(self): for command in self.commands: command()
- 深入淺出python閉包
- 複習
- 一等對象:指的是滿足下述條件的程式實體
- 在運作時建立
- 能指派給變量或資料結構中的元素
- 能作為參數傳給函數
- 能作為函數的傳回結果
- 整數、字元串和字典都是一等對象。在面向對象程式設計中,函數也是對象,并滿足以上條件,是以函數也是一等對象,稱為"一等函數"
- 普通函數 & 高階函數:接受函數為參數的函數為高階函數,其餘為普通函數
- 《設計模式:可複用面向對象軟體的基礎》書中的兩個設計原則:
- 對接口程式設計,而不是對實作程式設計
- 優先使用對象組合,而不是類繼承
- 一等對象:指的是滿足下述條件的程式實體
7. 函數裝飾器和閉包
- 函數裝飾器用于在源碼中“标記”函數,以某種方式增強函數的行為。這是一項強大的功能,但是若想掌握,必須了解閉包。
- 除了在裝飾器中有用處之外,閉包還是回調式異步程式設計和函數式程式設計風格的基礎。
7.1 裝飾器基礎
-
裝飾器是可調用的對象,其參數是另一個函數(被裝飾的函數)。裝飾器可能會處理被裝飾的函數,然後把它傳回,或者将其替換成另一個函數或可調用對象。
下述兩個代碼等價:
- 裝飾器
@decorate def target(): print('running target()')
- 另一種寫法
def target(): print('running target()') target = decorate(target)
- 裝飾器
- 兩種寫法的最終結果一樣:上述兩個代碼片段執行完畢後得到的 target 不一定是原來那個 target 函數,而是 decorate(target) 傳回的函數。
- 裝飾器通常把函數替換成另一個函數
def deco(func): def inner(): print('running inner()') return inner @deco def target(): print('running target()') target() # running inner() target # <function __main__.deco.<locals>.inner()>
- 嚴格來說,裝飾器隻是文法糖。
- 裝飾器的特性:
- 一大特性是,能把被裝飾的函數替換成其他函數。
- 第二個特性是,裝飾器在加載子產品時立即執行。
- Python何時執行裝飾器:在被裝飾的函數定義之後立即運作
registry = [] def register(func): print('running register(%s)' % func) registry.append(func) return func @register def f1(): print('running f1()') @register def f2(): print('running f2()') def f3(): print('running f3()') def main(): print('running main()') print('registry ->', registry) f1() f2() f3() if __name__ == '__main__': main()
running register(<function f1 at 0x00000246ABFAC708>) running register(<function f2 at 0x00000246AC0D4CA8>) running main() registry -> [<function f1 at 0x00000246ABFAC708>, <function f2 at 0x00000246AC0D4CA8>] running f1() running f2() running f3()
- 如果導入上述代碼:
,函數的裝飾器再導入子產品時立即執行,而被裝飾的函數隻再明确調用時運作。import registration
- 裝飾器在真實代碼中的常用方式:
- 裝飾器通常在一個子產品中定義,然後應用到其他子產品中的函數上
- 大多數裝飾器會在内部定義函數,然後将其傳回
- register 裝飾器原封不動地傳回被裝飾的函數,但是這種技術并非沒有用處。
- 很多 Python Web 架構使用這樣的裝飾器把函數添加到某種中央注冊處,例如把 URL 模式映射到生成 HTTP 響應的函數上的注冊處。
- 這種注冊裝飾器可能會也可能不會修改被裝飾的函數。
- 使用裝飾器改進“政策”模式
- 多數裝飾器會修改被裝飾的函數
- 通常,它們會定義一個内部函數,然後将其傳回,替換被裝飾的函數
- 使用内部函數的代碼幾乎都要靠閉包才能正确運作
7.2 閉包
- 變量作用域規則
- Python 不要求聲明變量,但是假定在函數定義體中指派的變量是局部變量
- 如果在函數中指派時想讓解釋器把 b 當成全局變量,要使用
聲明global
- 通過
檢視程式的位元組碼from dis import dis
- 假如有個名為 avg 的函數,它的作用是計算不斷增加的系列值的均值;例如,整個曆史中某個商品的平均收盤價。每天都會增加新價格,是以平均值要考慮至目前為止所有的價格
- avg使用方法:
>>> avg(10) 10.0 >>> avg(11) 10.5 >>> avg(12) 11.0
- 實作方式:
- 計算移動平均的類
class Averager(): def __init__(self): self.series = [] def __call__(self, new_value): self.series.append(new_value) total = sum(self.series) return total/len(self.series)
- 計算移動平均值的高階函數
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager
- **注意:**這兩個示例有共通之處:調用
或Averager()
得到一個可調用對象 avg,它會更新曆史值,然後計算目前均值make_averager()
- 在
函數中,averager
時自由變量(free variable),這是一個技術術語,指:未在本地作用域中綁定的變量series
- averager 的閉包延伸到那個函數的作用域之外,包含自由變量 series 的綁定
- 審查傳回的 averager 對象,我們發現 Python 在
屬性(表示編譯後的函數定義體)中儲存局部變量和自由變量的名稱__code__
avg.__code__.co_varnames # ('new_value', 'total') avg.__code__.co_freevars # ('series',)
- series 的綁定在傳回的 avg 函數的
屬性中。__closure__
中的各個元素對應于avg.__closure__
中的一個名稱。這些元素是 cell 對象,有個avg.__code__.co_freevars
屬性,儲存着真正的值cell_contents
avg.__closure__ # (<cell at 0x00000246AC517108: list object at 0x00000246AC124A88>,) avg.__closure__[0].cell_contents # [10, 11, 12]
- 在
- 計算移動平均的類
- avg使用方法:
- 閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,這樣調用函數時,雖然定義作用域不可用了,但是仍能使用那些綁定。
- 注意,隻有嵌套在其他函數中的函數才可能需要處理不在全局作用域中的外部變量。
- nonlocal聲明
- 上面的操作時引入了list(可變對象) series,用來儲存每一次的值,然後這個series 是 所謂的 自由變量
- 但是對數字、字元串、元組等不可變類型來說,隻能讀取,不能更新,就不是所謂的 自由變量了,是以不會儲存在閉包中。
-
聲明可以把變量标記為自由變量,即使在函數中為變量賦予新值了,也會變成自由變量。nonlocal
- 如果為
聲明的變量賦予新值,閉包中儲存的綁定會更新。nonlocal
- 優化後的閉包
def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count, total count += 1 total += new_value return total/count return averager
- nonlocal是python3的特性,在python2中需要把内部函數需要修改的變量存儲為可變對象(如字典或簡單的執行個體)的元素或屬性,并且把那個對象綁定給一個自由變量。
7.3 裝飾器
- 實作一個簡單的裝飾器:定義了一個裝飾器,它會在每次調用被裝飾的函數時計時,然後把經過的時間、傳入的參數和調用的結果列印出來。
import time def clock(func): def clocked(*args): # 記錄初始時間t0 t0 = time.perf_counter() # 調用原來的 factorial 函數,儲存結果 result = func(*args) # 計算經過的時間 elapsed = time.perf_counter() - t0 # 格式化收集的資料,然後列印出來 name = func.__name__ arg_str = ', '.join(repr(arg) for arg in args) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) # 傳回第 2 步儲存的結果 return result return clocked @clock def snooze(seconds): time.sleep(seconds) @clock def factorial(n): return 1 if n < 2 else n * factorial(n - 1) if __name__ == '__main__': print('*' * 40, 'Calling snooze(.123)') snooze(.123) print('*' * 40, 'Calling factorial(6)') print('6! = ', factorial(6))
**************************************** Calling snooze(.123) [0.12354480s] snooze(0.123) -> None **************************************** Calling factorial(6) [0.00000040s] factorial(1) -> 1 [0.00003500s] factorial(2) -> 2 [0.00004590s] factorial(3) -> 6 [0.00005500s] factorial(4) -> 24 [0.00006390s] factorial(5) -> 120 [0.00007470s] factorial(6) -> 720 6! = 720
- 這是裝飾器的典型行為:把被裝飾的函數替換成新函數,二者接受相同的參數,而且(通常)傳回被裝飾的函數本該傳回的值,同時還會做些額外操作。
裝飾器:動态地給一個對象添加一些額外的職責
——《設計模式:可複用面向對象軟體的基礎》
- 上述實作的clock裝飾器有幾個缺點:
- 不支援關鍵字參數
- 遮蓋了被裝飾函數的
和__name__
屬性__doc__
- 使用
裝飾器把相關屬性從func複制到clocked中,同時還可以正确處理關鍵字參數functools.wraps
import time from functools import wraps def clock(func): @wraps(func) def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - t0 name = func.__name__ arg_lst = [] if args: arg_lst.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked
- 标準庫中的裝飾器
- python 内置了三個用于裝飾方法的函數:property、classmethod、staticmethod
- functools:wraps、lru_cache、singledispatch
- 使用
做備忘,memoization,這是一項優化技術,它把耗時的函數的結果儲存起來,避免傳入相同的參數時重複計算。functools.lru_cache
- LRU:Least Recently Used,表明緩存不會無限制增長,一段時間不用的緩存條目會被扔掉。
- 注意,必須像正常函數哪一行調用
:lru_cache
,這是因為@functools.lru_cache()
可以接受配置參數。lru_cache
- 除了優化遞歸算法之外,
在從Web中擷取資訊的應用中也能發揮巨大作用。lru_cache
-
functools.lru_cache(maxsize=128, typed=False)
- maxsize 參數指定存儲多少個調用的結果。緩存滿了之後,舊的結果會被扔掉,騰出空間。為了得到最佳性能,maxsize 應該設為 2 的幂。
- typed 參數如果設為 True,把不同參數類型得到的結果分開儲存,即把通常認為相等的浮點數和整數參數(如 1 和 1.0)區分開。
- lru_cache 使用字典存儲結果,而且鍵根據調用時傳入的定位參數和關鍵字參數建立,是以被 lru_cache 裝飾的函數,它的所有參數都必須是可散列的。
- 單分派泛函數
- 因為 Python 不支援重載方法或函數,是以我們不能使用不同的簽名定義htmlize 的變體,也無法使用不同的方式處理不同的資料類型。
- 在Python 中,一種常見的做法是把 htmlize 變成一個分派函數,使用一串 if/elif/elif,調用專門的函數,如htmlize_str、htmlize_int,等等。這樣不便于子產品的使用者擴充,還顯得笨拙:時間一長,分派函數 htmlize 會變得很大,而且它與各個專門函數之間的耦合也很緊密。
-
裝飾器可以把整體方案拆分成多個子產品,甚至可以為你無法修改的類提供專門的函數。functools.singledispatch
- 使用
裝飾的普通函數會變成泛函數(generic function):根據第一個參數的類型,以不同方式執行相同操作的一組函數。@singledispatch
這才稱得上是單分派。如果根據多個參數選擇專門的函數,那就是多分派了。
- singledispatch 建立一個自定義的htmlize.register 裝飾器,把多個函數綁在一起組成一個泛函數
from functools import singledispatch from collections import abc import numbers import html @singledispatch def htmlize(obj): content = html.escape(repr(obj)) return '<pre>{}</pre>'.format(content) @htmlize.register(str) def _(text): content = html.escape(text).replace('\n', '<br>\n') return '<p>{0}</p>'.format(content) @htmlize.register(numbers.Integral) def _(n): return '<pre>{0} (0x{0:x})</pre>' @htmlize.register(tuple) @htmlize.register(abc.MutableSequence) def _(seq): inner = '</li>\n<li>'.join(htmlize(item) for item in seq) return '<ul>\n<li>' + inner + '</li>\n</ul>'
-
是numbers.Integral
的虛拟超類int
- 可以疊放多個
裝飾器,讓同一個函數支援不同類型register
- 隻要可能,注冊的專門函數應該處理抽象基類(如 numbers.Integral和 abc.MutableSequence),不要處理具體實作(如 int 和 list)。這樣,代碼支援的相容類型更廣泛。
- 使用抽象基類檢查類型,可以讓代碼支援這些抽象基類現有和未來的具體子類或虛拟子類
-
不是為了把 Java 的那種方法重載帶入Python@singledispatch
- 在一個類中為同一個方法定義多個重載變體,比在一個函數中使用一長串 if/elif/elif/elif 塊要更好。
- 但是這兩種方案都有缺陷,因為它們讓代碼單元(類或函數)承擔的職責太多。
-
的優點是支援子產品化擴充:各個子產品可以為它支援的各個類型注冊一個專門函數。@singledispath
- 裝飾器是函數,是以可以組合起來使用(即,可以在已經被裝飾的函數上應用裝飾器)
-
疊放裝飾器
下述代碼等價
@d1 @d2 def f(): print('f') f = d1(d2(f))
- 參數化裝飾器
registry = set() def register(active=True): def decorate(func): print('running register(active=%s )->decorate(%s)' % (active, func)) if active: registry.add(func) else: registry.discard(func) return func return decorate @register(active=False) def f1(): print('running f1()') @register() def f2(): print('running f2()') def f3(): print('running f3()')
- 這裡decorate時裝飾器,必須傳回一個函數
- register是裝飾器工廠函數,是以傳回的是一個裝飾器decorate
- 即使不傳入參數,register也必須作為函數調用:
@register()
- 如果不使用 @ 句法,那就要像正常函數那樣使用 register;若想把 f 添加到 registry 中,則裝飾 f 函數的句法是
;不想添加(或把它删除)的話,句法是register()(f)
register(active=False)(f)
- 參數化clock裝飾器
import time DEFAULT_FMT = '[{elapsed:0.8f}s] {name}({args}) -> {result}' def clock(fmt=DEFAULT_FMT): def decorate(func): def clocked(*_args): t0 = time.time() _result = func(*_args) elapsed = time.time() - t0 name = func.__name__ args = ", ".join(repr(arg) for arg in _args) result = repr(_result) print(fmt.format(**locals())) return _result return clocked return decorate if __name__ == '__main__': @clock() def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)
[0.12328148s] snooze(0.123) -> None [0.12369442s] snooze(0.123) -> None [0.12362432s] snooze(0.123) -> None
- clock:參數化裝飾器工廠函數
- decorate:真正的裝飾器
- clocked:包裝被裝飾的函數
-
:被裝飾函數傳回的真正結果_result
-
:clocked的參數,args用于顯示的字元串_args
- result:
的字元串表現形式,用于顯示_result
- 裝飾器的參數
@clock('{name}: {elapsed}s') def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)
snooze: 0.12300252914428711s snooze: 0.12314867973327637s snooze: 0.1238546371459961s
@clock('{name}({args}) dt={elapsed:0.3f}s') def snooze(seconds): time.sleep(seconds) for i in range(3): snooze(.123)
snooze(0.123) dt=0.124s snooze(0.123) dt=0.123s snooze(0.123) dt=0.123s
8. 對象引用、可變性和垃圾回收
8.1 變量不是盒子
- 如果把變量想象為盒子,那麼無法解釋 Python 中的指派;應該把變量視作便利貼
- 對引用式變量來說,說把變量配置設定給對象更合理
-
運算符比較兩個對象的值(對象中儲存的資料),而==
比較對象的辨別is
- 元組的相對不可變性:元組的不可變性其實是指
tuple
資料結構的實體内容(即儲存的引用)不可變,與引用的對象無關。
元組裡面有一個list,這個list就可以append,但是id還是不變。
這也是有些元組不可散列的原因。
- str、bytes 和 array.array 等單一類型序列是扁平的,它們儲存的不是引用,而是在連續的記憶體中儲存資料本身(字元、位元組和數字)。
- 淺複制(copy.copy)和深複制(copy.deepcopy)的差別:深複制中副本不共享内部對象的引用
8.2 函數的參數作為引用
- 不要使用可變類型作為參數的預設值
- 不可以預設值為類似于
,因為如果不傳參的話,這個[]
就是類的内部變量,多個執行個體會共用這個變量[]
- 這也是為什麼通常使用 None 作為接收可變值的參數的預設值的原因。
- 不可以預設值為類似于
- 防禦可變參數
- 類中的操作可能會修改傳入類的可變參數
- 修正的方法:初始化時,把參數值的副本指派給成員變量
8.3 del、垃圾回收和弱引用
注意,這一章節的一些代碼要在cmd中才能實作,jupyterlab中的實驗結果與書中給的結果不一緻。
- del 語句删除名稱,而不是對象
- del 指令可能會導緻對象被當作垃圾回收,但是僅當删除的變量儲存的是對象的最後一個引用,或者無法得到對象時。
- 重新綁定也可能會導緻對象的引用數量歸零,導緻對象被銷毀。
- 弱引用:正是因為有引用,對象才會在記憶體中存在。當對象的引用數量歸零後,垃圾回收程式會把對象銷毀。但是,有時需要引用對象,而不讓對象存在的時間超過所需時間。這經常用在緩存中。
- 弱引用不會增加對象的引用數量。引用的目标對象稱為所指對象(referent)。是以我們說,弱引用不會妨礙所指對象被當作垃圾回收。
- 弱引用在緩存應用中很有用,因為我們不想僅因為被緩存引用着而始終儲存緩存對象。
- weakref.ref 類其實是低層接口,供進階用途使用,多數程式最好使用 weakref 集合和 finalize。也就是說,應該使用
、WeakKeyDictionary
、WeakValueDictionary
和WeakSet
(在内部使用弱引用),不要自己動手建立并處理 weakref.ref 執行個體。finalize
- WeakValueDictionary
- WeakValueDictionary 類實作的是一種可變映射,裡面的值是對象的弱引用。
- 被引用的對象在程式中的其他地方被當作垃圾回收後,對應的鍵會自動從 WeakValueDictionary 中删除。
- 是以,WeakValueDictionary 經常用于緩存。
- 與 WeakValueDictionary 對應的是 WeakKeyDictionary,後者的鍵是弱引用
- WeakSet 類:儲存元素弱引用的集合類。元素沒有強引用時,集合會把它删除
- 如果一個類需要知道所有執行個體,一種好的方案是建立一個WeakSet 類型的類屬性,儲存執行個體的引用。
- 如果使用正常的 set,執行個體永遠不會被垃圾回收,因為類中有執行個體的強引用,而類存在的時間與 Python 程序一樣長,除非顯式删除類。
- 弱引用的局限
- 不是每個 Python 對象都可以作為弱引用的目标(或稱所指對象)。基本的 list 和 dict 執行個體不能作為所指對象,但是它們的子類可以
- set 執行個體可以作為所指對象
- 使用者定義的類型也沒問題
- 但是,int 和 tuple 執行個體不能作為弱引用的目标,甚至它們的子類也不行
- 這些局限基本上是 CPython 的實作細節,在其他 Python 解釋器中情況可能不一樣。這些局限是内部優化導緻的結果
- Python對不可變類型施加的把戲
- 對元組 t 來說,
不建立副本,而是傳回同一個對象的引用。此外,t[:]
獲得的也是同一個元組的引用。(str、bytes 和 frozenset 執行個體也有這種行為)tuple(t)
- frozenset 執行個體不是序列,是以不能使用
(fs 是一個 frozenset 執行個體),但是,fs[:]
具有相同的效果:傳回同一個對象的引用,而不是建立一個副本fs.copy()
- frozenset 執行個體不是序列,是以不能使用
- 字元串字面量可能會建立共享的對象
s1 = 'ABC' s2 = 'ABC' s2 is s1 # True
- 共享字元串字面量是一種優化措施,稱為駐留(interning)。CPython 還會在小的整數上使用這個優化措施,防止重複建立“熱門”數字
- CPython 不會駐留所有字元串和整數
- 對元組 t 來說,
- 雜談
- java的
運算符比較的是對象(不是基本類型)的引用,而不是對象的值。否則的話要用到==
方法,如果調用方法的變量為.equals
,會得到一個 空指針異常null
- 在python中,
比較對象的值,==
比較引用is
- 最重要的是,python支援重載運算發,
能正确處理标準庫中的所有對象,包括==
,與java的null不同None
- java的
9. 符合Python風格的對象
9.1 對象的表示形式
- 擷取對象的字元串表示形式的标準方式
-
:開發者了解的方式傳回對象的字元串表示形式repr()
-
:使用者了解的方式傳回對象的字元串表示形式str()
-
- 在 Python 3 中,
、__repr__
和__str__
都必須傳回 Unicode 字元串(str 類型)。隻有__format__
方法應該傳回位元組序列(bytes 類型)__bytes__
- 定義
方法,把類的執行個體程式設計可疊代的對象,這樣才能拆包。__iter__
x, y = my_vector
這一行也可以寫成
yield self.x; yield self.y
-
函數用來執行一個字元串表達式,并傳回表達式的值。eval()
- classmethod:第一個參數是類本身,最常見的用途是定義備選構造方法
- staticmethod:第一個參數不是特殊的值,其實靜态方法就是普通的函數,隻是碰巧在類的定義體中,而不是在子產品層定義
- 作者對staticmethod的态度是:”不是特别有用“,因為如果想定義不需要與類進行互動的函數,隻需要在子產品中定義就好了。
9.2 格式化顯示
- 内置的
函數和format()
方法把各個類型的格式化方式委托給相應的str.format()
方法.__format__(format_spec)
-
裡的第二個參數format(my_obj, format_spec)
-
方法的格式字元串,str.format()
裡代換字段中冒号後面的部分{}
-
-
這樣的格式字元串其實包含兩部分{0.mass:5.3e}
- 冒号左邊的
在代換字段句法中是字段名.mass
- 冒号後面的
是格式說明符5.3e
- 格式說明符使用的表示法叫 格式規範微語言 ,Format Specification Mini-Language
- 冒号左邊的
- 格式規範微語言為一些内置類型提供了專用的表示代碼
- b 和 x 分别表示二進制和十六進制的 int 類型
- f 表示小數形式的 float 類型
- % 表示百分數形式
- 格式規範微語言是可擴充的,因為各個類可以自行決定如何解釋format_spec 參數
- 如果類沒有定義
方法,從 object 繼承的方法會傳回__format__
str(my_object)
- 在格式規範微語言中,整數使用的代碼有
,浮點數使用的代碼有bcdoxXn
,字元串使用的代碼有eEfFgGn%
s
- 使用兩個前導下劃線(尾部沒有下劃線,或者有一個下劃線),把屬性标記為私有的
-
裝飾器把讀值方法标記為屬性@property
- 要想建立可散列的類型,不一定要實作特性,也不一定要保護執行個體屬性。隻需正确地實作
和__hash__
方法即可__eq__
- 如果定義的類型有标量數值,可能還要實作
和__int__
方法(分别被__float__
和int()
構造函數調用),以便在某些情況下用于強制轉換類型。此外,還有用于支援内置的float()
構造函數的complex()
方法__complex__
from array import array import math class Vector2d: typecode = 'd' def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property def x(self): return self.__x @property def y(self): return self.__y def __iter__(self): return (i for i in (self.x, self.y)) def __repr__(self): class_name = type(self).__name__ return '{}({!r}, {!r})'.format(class_name, *self) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.hypot(self.x, self.y) def __bool__(self): return bool(abs(self)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) def angle(self): return math.atan2(self.y, self.x) def __hash__(self): return hash(self.x) ^ hash(self.y) def __format__(self, fmt_spec=''): if fmt_spec.endswith('p'): fmt_spec = fmt_spec[: -1] coords = (abs(self), self.angle()) outer_fmt = '<{}, {}>' else: coords = self outer_fmt = '({}, {})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(*components)
9.3 Python的私有屬性和受保護的屬性
- 私有屬性需要在前面加兩根下劃線,Python 會把屬性名存入執行個體的
屬性中,而且會在前面加上一個下劃線和類名__dict__
- 父類:_Dog__mood
- 子類:_Beagle__mood
- 這個語言特性叫 名稱改寫 (name mangling)
- Python的私有屬性是可以被修改的,通過上述名稱修改
- 受保護屬性:
- Python 解釋器不會對使用單個下劃線的屬性名做特殊處理,不過這是很多 Python 程式員嚴格遵守的約定,他們不會在類外部通路這種屬性。
- 不過在子產品中,頂層名稱使用一個前導下劃線的話,的确會有影響:對
來說,mymod 中字首為下劃線的名稱不會被導入。然而,依舊可以使用from mymod import *
将其導入。from mymod import _privatefunc
- 使用
類屬性節省空間__slots__
- 為了使用底層的散清單提升通路速度,字典會消耗大量記憶體。如果要處理數百萬個屬性不多的執行個體,通過
類屬性,能節省大量記憶體,方法是讓解釋器在元組中存儲執行個體屬性,而不用字典。__slots__
- 定義
的方式是,建立一個類屬性,使用__slots__
這個名字,并把它的值設為一個字元串構成的可疊代對象,其中各個元素表示各個執行個體屬性。__slots__
- 在類中定義
屬性的目的是告訴解釋器:“這個類中的所有執行個體屬性都在這兒了!”這樣,Python 會在各個執行個體中使用類似元組的結構存儲執行個體變量,進而避免使用消耗記憶體的__slots__
屬性。如果有數百萬個執行個體同時活動,這樣做能節省大量記憶體。__dict__
- 如果類中定義了
屬性,而且想把執行個體作為弱引用的目标,那麼要把__slots__
添加到'__weakref__'
中。__slots__
- 為了使用底層的散清單提升通路速度,字典會消耗大量記憶體。如果要處理數百萬個屬性不多的執行個體,通過
- 如果使用得當,
能顯著節省記憶體__slots__
- 每個子類都要定義
屬性,因為解釋器會忽略繼承的__slots__
屬性__slots__
- 執行個體隻能擁有
中列出的屬性,除非把__slots__
加入'__dict__'
中(這樣做就失去了節省記憶體的功效)__slots__
- 如果不把
加入'__weakref__'
,執行個體就不能作為弱引用的目标__slots__
- 每個子類都要定義
- 覆寫類屬性
- 類屬性可用于為執行個體屬性提供預設值
- 如果為不存在的執行個體屬性指派,會建立執行個體屬性
- 假如我們為
執行個體屬性指派,那麼同名 類屬性 不受影響typecode
- 然而,自此之後,執行個體讀取的
是執行個體屬性self.typecode
,也就是把同名類屬性遮蓋了typecode
- Python風格的修改方法:
- 類屬性是公開的,是以會被子類繼承
- 于是經常會建立一個子類,隻用于定制類的資料屬性
- 總結:
- 所有用于擷取字元串和位元組序清單示形式的方法:
、__repr__
、__str__
和__format__
。__bytes__
- 把對象轉換成數字的幾個方法:
、__abs__
和__bool__
。__hash__
- 用于測試位元組序列轉換和支援散列(連同
方法)的__hash__
運算符。__eq__
- 所有用于擷取字元串和位元組序清單示形式的方法:
10. 序列的修改、散列和切片
-
:展示變量的程式員能明白的語句reprlib.repr
- 協定和鴨子類型
- 在 Python 中建立功能完善的序列類型無需使用繼承,隻需實作符合序列協定的方法
- 在面向對象程式設計中,協定是非正式的接口,隻在文檔中定義,在代碼中不定義
- 例如,Python 的序列協定隻需要
和__len__
兩個方法__getitem__
- 協定是非正式的,沒有強制力,是以如果你知道類的具體使用場景,通常隻需要實作一個協定的部分。
-
:indices 方法開放了内置序列實作的棘手邏輯,用于優雅地處理缺失索引和負數索引,以及長度超過目标序列的切片。這個方法會“整頓”元組,把 start、stop 和 stride 都變成非負數,而且都落在指定長度序列的邊界内。slice.indices
-
:對__getattr__()
表達式,Python 會檢查my_obj.x
執行個體有沒有名為my_obj
的屬性;如果沒有,到類(x
)中查找;如果還沒有,順着繼承樹繼續查找。如果依舊找不到,調用my_obj.__class__
所屬類中定義的my_obj
方法,傳入__getattr__
和屬性名稱的字元串形式(如 ‘x’)self
- 不建議隻為了避免建立執行個體屬性而使用
屬性。__slots__
屬性隻應該用于節省記憶體,而且僅當記憶體嚴重不足時才應該這麼做。__slots__
- 歸約函數(reduce、sum、any、all)把序列或有限的可疊代對象變成一個聚合結果
-
的關鍵思想是,把一系列值歸約成單個值。functools.reduce()
函數的第一個參數是接受兩個參數的函數,第二個參數是一個可疊代的對象。reduce()
import functools functools.reduce(lambda a, b: a * b, range(1, 6)) # 120
-
:使用的時候最好提供第三個參數,這樣能避免異常。如果序列為空,reduce(function, iterable, initializer)
是傳回的結果;否則,在歸約中使用它作為第一個參數,是以應該使用恒等值。比如,對 +、| 和 ^ 來說,initializer
應該是 0;而對 * 和 & 來說,應該是 1。initializer
-
:使用 zip 函數能輕松地并行疊代兩個或更多可疊代對象,它傳回的元組可以拆包成變量,分别對應各個并行輸入中的一個元素。zip
- zip 有個奇怪的特性:當一個可疊代對象耗盡後,它不發出警告就停止
-
函數的行為有所不同:使用可選的itertools.zip_longest
(預設值為 None)填充缺失的值,是以可以繼續産出,直到最長的可疊代對象耗盡fillvalue
from array import array import reprlib import math import numbers import functools import operator import itertools class Vector: typecode = 'd' def __init__(self, components): self._components = array(self.typecode, components) def __iter__(self): return iter(self._components) def __repr__(self): components = reprlib.repr(self._components) components = components[components.find('['):-1] return 'Vector({})'.format(components) def __str__(self): return str(tuple(self)) def __bytes__(self): return (bytes([ord(self.typecode)]) + bytes(self._components)) def __eq__(self, other): return tuple(self) == tuple(other) def __abs__(self): return math.sqrt(sum(x * x for x in self)) def __bool__(self): return bool(abs(self)) def __len__(self): return len(self._components) # def __getitem__(self, index): # return self._components[index] def __getitem__(self, index): cls = type(self) # 如果傳入的參數是切片,就傳回改切片生成的Vector類 if isinstance(index, slice): return cls(self._components[index]) # 如果傳入的參數是數,就傳回清單中的元素 elif isinstance(index, numbers.Integral): return self._components[index] # 否則就報錯啦 else: msg = '{cls.__name__} indices must be integers' raise TypeError(msg.format(cls=cls)) shortcut_names = 'xyzt' def __getattr__(self, name): cls = type(self) if len(name) == 1: pos = cls.shortcut_names.find(name) if 0 <= pos < len(self._components): return self._components[pos] msg = '{.__name__!r} object has no attribute {!r}' raise AttributeError(msg.format(cls, name)) def __setattr__(self, name, value): cls = type(self) if len(name) == 1: if name in cls.shortcut_names: error = 'readonly attribute {attr_name!r}' elif name.islower(): error = "can't set attributes 'a' to 'z' in {cls_name!r}" else: error = '' if error: msg = error.format(cls_name=cls.__name__, attr_name=name) raise AttributeError(msg) super().__setattr__(name, value) def __eq__(self, other): # return tuple(self) == tuple(other) # 為了提高比較的效率,使用zip函數 # if len(self) != len(other): # return False # for a, b in zip(self, other): # if a != b: # return False # return True # 過用于計算聚合值的整個 for 循環可以替換成一行 all 函數調用: # 如果所有分量對的比較結果都是 True,那麼結果就是 True。 return len(self) == len(other) and all(a == b for a, b in zip(self, other)) def __hash__(self): # 建立一個生成器表達式,惰性計算各個分量的散列值 # hashes = (hash(x) for x in self._components) # 這裡換成map方法 hashes = map(hash, self._components) # 把hashes提供給reduce函數,第三個參數0是初始值 return functools.reduce(operator.xor, hashes, 0) def angle(self, n): r = math.sqrt(sum(x * x for x in self[n:])) a = math.atan2(r, self[n-1]) if (n == len(self) - 1) and (self[-1] < 0): return math.pi * 2 - a else: return a def angles(self): return (self.angle(n) for n in range(1, len(self))) def __format__(self, fmt_spec=''): if fmt_spec.endswith('h'): # 超球面坐标 fmt_spec = fmt_spec[:-1] coords = itertools.chain([abs(self)], self.angles()) outer_fmt = '<{}>' else: coords = self outer_fmt = '({})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(', '.join(components)) @classmethod def frombytes(cls, octets): typecode = chr(octets[0]) memv = memoryview(octets[1:]).cast(typecode) return cls(memv)
11. 接口:從協定到抽象基類
鴨子類型:“當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”
程式設計中,鴨子類型(英語:duck typing)是 動态類型 的一種風格。在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實作特定的接口,而是由"目前方法" (計算機科學)和屬性的集合決定。
在鴨子類型中,關注點在于對象的行為,能做什麼;而不是關注對象所屬的類型。
- Python文化中的接口和協定
- 受保護的屬性和私有屬性不在接口中
- 接口 的補充定義:對象公開方法的子集,讓對象在系統中扮演特定的角色
- 協定與繼承沒有關系。一個類可能會實作多個接口,進而讓執行個體扮演多個角色。
- 協定是接口,但不是正式的,是以協定不能像正式接口那樣施加限制
- 一個類可能隻實作部分接口,這是允許的
- 對 Python 程式員來說,“X 類對象”、“X 協定”和“X 接口”都是一個意思
- 序列協定是 Python 最基礎的協定之一。即便對象隻實作了那個協定最基本的一部分,解釋器也會負責任地處理。
- Python喜歡序列
- 定義為抽象基類的Sequence正式接口
- 定義
方法,隻實作序列協定的一部分,這樣足夠通路元素、疊代和使用 in 運算符了__getitem__
class Foo: def __getitem__(self, pos): return range(0, 30, 10)[pos]
- 雖然沒有
方法,但是 Foo 執行個體是可疊代的對象,因為發現有__iter__
方法時,Python 會調用它,傳入從 0 開始的整數索引,嘗試疊代對象__getitem__
- 鑒于序列協定的重要性,如果沒有
和__iter__
方法,Python 會調用__contains__
方法,設法讓疊代和 in 運算符可用。__getitem__
- 雖然沒有
- shuffle 函數要調換集合中元素的位置,隻實作了
即是隻實作了不可變的序列協定,可變的序列還必須提供__getitem__
方法。__setitem__
- 猴子更新檔:在運作時修改類或子產品,而不改動源碼
- 協定是動态的:
函數不關心參數的類型,隻要那個對象實作了部分可變序列協定即可。即便對象一開始沒有所需的方法也沒關系,後來再提供也行。random.shuffle
- “鴨子類型”:對象的類型無關緊要,隻要實作了特定的協定即可。
- Alex Martelli的水禽
- 用isinstance檢查對象的類型,而不是
。type(foo) is bar
- 白鵝類型,goose typing,指隻要
是抽象基類,即cls
的元類是cls
,就可以使用abc.ABCMeta
isinstance(obj, cls)
- Python 的抽象基類還有一個重要的實用優勢:可以使用
類方法在終端使用者的代碼中把某個類“聲明”為一個抽象基類的“虛拟”子類register
- 無需注冊,
也能把Struggle識别為自己的子類,隻要實作了特殊方法abc.Sized
即可。要使用正确的句法和語義實作,前者要求沒有參數,後者要求傳回一個非負整數,指明對象的長度。如果不使用規範的文法和語義實作特殊方法,如__len__
,會導緻非常嚴重的問題__len__
class Struggle: def __len__(self): return 23 from collections import abc isinstance(Struggle(), abc.Sized)
- 在 Python 3.4 中沒有能把字元串和元組或其他不可變序列區分開的抽象基類,是以必須測試 str:
。isinstance(x, str)
- EAFP和LBYL
- EAFP:Easier to Ask for Forgiveness than Permission,請求寬恕比許可更容易
- 操作前不檢查,出了問題由異常處理來處理
- 代碼表現:try…except…
try: x = test_dict["key"] except KeyError: # key 不存在
- LBYL:Look Before You Leap,三思而後行
- 操作前先檢查,再執行
- 代碼表現:if…else…
if "key" in test_dict: x = test_dict["key"] else: # key 不存在
- EAFP 的異常處理往往也會影響一點性能,因為在發生異常的時候,程式會進行保留現場、回溯traceback等操作,但在異常發生頻率比較低的情況下,性能相差的并不是很大。
- 而 LBYL 則會消耗更高的固定成本,因為無論成敗與否,總是執行額外的檢查。
- 相比之下,如果不引發異常,EAFP 更優一些,
- Python 的動态類型(duck typing)決定了 EAFP,而 Java等強類型(strong typing)決定了 LBYL
- EAFP:Easier to Ask for Forgiveness than Permission,請求寬恕比許可更容易
- 定義抽象基類的子類
- 導入時,Python不會檢查抽象方法的實作,在運作時執行個體化類的時候才會真正檢查。是以,如果沒有正确實作某個抽象方法,Python會抛出TypeError異常。
- MutableSequence 抽象基類和 collections.abc 中它的超類的UML類圖(箭頭由子類指向祖先;以斜體顯示的名稱是抽象類和抽象方法)
- 标準庫中的抽象基類
-
子產品中各個抽象基類的 UML 類圖collections.abc
- Iterable、Container 和 Sized
- 各個集合應該繼承這三個抽象基類,或者至少實作相容的協定。
- Iterable 通過
方法支援疊代__iter__
- Container 通過
方法支援 in 運算符__contains__
- Sized 通過
方法支援__len__
函數len()
- Sequence、Mapping 和 Set
- 這三個是主要的不可變集合類型,而且各自都有可變的子類
- MappingView
- 映射方法 .items()、.keys() 和 .values() 傳回的對象分别是 ItemsView、KeysView 和 ValuesView 的執行個體
- Callable 和 Hashable
- 這兩個抽象基類與集合沒有太大的關系,隻不過因為
是标準庫中定義抽象基類的第一個子產品,而它們又太重要了,是以才把它們放到collections.abc
子產品中collections.abc
- 這兩個抽象基類的主要作用是為内置函數
提供支援,以一種安全的方式判斷對象能不能調用或散列isinstance
- 若想檢查是否能調用,可以使用内置的 callable() 函數;但是沒有類似的 hashable() 函數,是以測試對象是否可散列,最好使用
isinstance(my_obj, Hashable)
- 這兩個抽象基類與集合沒有太大的關系,隻不過因為
- Iterator
- Iterable 的子類
-
- 抽象基類的金字塔
- numbers包定義的是 數字塔
- Number
- Complex
- Real
- Rational
- Integral
- 檢查一個數是不是整數:
,這樣代碼就可以接受int、boolisinstance(x, numbers.Integral)
- 檢查一個數是不是浮點數:
,這樣代碼就可以接受bool、int、float、fractions.Fraction,還有外部庫如Numpyisinstance(x, number.Real)
- deciaml.Decimal沒有注冊為numbers.Real的虛拟子類,是因為,如果你的程式需要Decimal的精度,要防止與其他低精度數字類型混合,尤其是浮點數
- numbers包定義的是 數字塔
- 定義并使用一個抽象基類
- 抽象方法示例:
import abc class Tombola(abc.ABC): @abc.abstractmethod def load(self, iterable): """從可疊代對象中添加元素""" @abc.abstractmethod def pick(self): """随機删除元素,然後将其傳回 如果執行個體為空,這個方法應該抛出 LookupError """ def loaded(self): """如果至少有一個元素,傳回True,否則傳回False""" return bool(self.inspect()) def inspect(self): """傳回一個有序元組,由目前元素構成""" items = [] while True: try: items.append(self.pick()) except LookupError: break self.load(items) return tuple(sorted(items))
- 抽象方法可以有實作代碼。即便實作了,子類也必須覆寫抽象方法,但是在子類中可以使用 super() 函數調用抽象方法,為它添加功能,而不是從頭開始實作
- IndexEror和KeyError是LookupError的子類
- IndexError:嘗試從序列中擷取索引超過最後位置的元素時抛出
- KeyError:使用不存在的鍵從映射中擷取元素時,抛出 KeyError 異常
- 抽象方法示例:
- 抽象基類文法詳解
- 可以通過裝飾器堆疊的方式,聲明抽象類方法、抽象靜态方法、抽象屬性等。
class MyABC(abc.ABC): @classmethod @abc.abstractmethod def an_abstract_classmethod(cls, ...): pass
- 與其他方法描述符一起使用時,
應該放在 最裡層abstractmethod()
- 唯一推薦使用的抽象基類方法裝飾器是@abstractmethod,其他裝飾器已經廢棄了
- 可以通過裝飾器堆疊的方式,聲明抽象類方法、抽象靜态方法、抽象屬性等。
- 定義Tombola抽象基類的子類
- 就算iterable參數始終傳入清單,
會建立參數的副本,這依然是好的做法list(iterable)
- 白鵝類型 的重要動态特性:使用register方法聲明虛拟子類
- Python中的鴨子類型和白鵝類型
鴨子類型與繼承毫無關系。
-
鴨子類型的定義是:
“當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。”
-
言簡意赅的了解是:
“對象的類型無關緊要,隻要實作了特定的協定即可。忽略對象真正的類型,轉而關注對象有沒有實作所需的方法、簽名和語義。”
- 最直接的結果就是:
- 一個使用者定義的類型,不需要真正的繼承自抽象基類,但是卻可以當作其子類一樣來使用。
- 比如使用者實作了序列協定,就可以當作内置序列類型來用,對其使用
等函數,調用len()
等用于内置類型的方法。__len__()
- 比如使用者實作了
方法,Python就可以直接去疊代這個類型。__getitem__
- Python内置庫和第三方的庫,雖然是針對Python的類型設計的,但是都可以直接用于使用者自定義的類型上。
-
白鵝類型的定義是:
使用抽象基類明确聲明接口,子類顯示地繼承抽象基類,抽象基類會檢查子類是否符合接口定義。
-
這樣做的劣勢是:
子類為了經過抽象基類的接口檢查,必須實作一些接口,但是這些接口你可能用不到。
-
這樣做的優勢是:
一些直接繼承自抽象基類的接口是可以拿來即用的。
-
- 就算iterable參數始終傳入清單,
- Tombola的虛拟子類
@Tombola.register class TomboList(list): def pick(self): if self: position = randrange(len(self)) return self.pop(position) else: raise LookupError('pop from empty TomboList') load = list.extend def loaded(self): return bool(self) def inspect(self): return tuple(sorted(self)) # Tombola.register(TomTomboList)
- 白鵝類型的一個基本特性:即便不繼承,也有辦法把一個類注冊為抽象基類的虛拟子類。
- 注冊虛拟子類的方式是在抽象基類上調用 register 方法
- 這麼做之後,注冊的類會變成抽象基類的虛拟子類,而且 issubclass 和isinstance 等函數都能識别
- 但是注冊的類不會從抽象基類中繼承任何方法或屬性
- 虛拟子類不會繼承注冊的抽象基類,而且任何時候都不會檢查它是否符合抽象基類的接口,即便在執行個體化時也不會檢查。為了避免運作時錯誤,虛拟子類要實作所需的全部方法。
-
,load跟list的extend一樣。load = list.extend
- 類的繼承關系在一個特殊的類屬性中指定——
,即方法解析順序(Method Resolution Order)__mro__
- 這個屬性的作用很簡單,按順序列出類及其超類,Python 會按照這個順序搜尋方法
- 它隻列出了“真實的”超類,利用
內建的不在其中,是以也沒有從中繼承任何方法@類名.register
- Tombola子類的測試方法
-
:這個方法傳回類的直接子類清單,不含虛拟子類__subclasses__()
-
:隻有抽象基類有這個資料屬性,其值是一個 WeakSet 對象,即抽象類注冊的虛拟子類的弱引用_abc_registry
-
- 問題:報錯沒有找到
,_abc_registry
- 這是因為,python3.7版本沒有,需要用3.4才行,參考連結
- 并且在3.7的版本中是不能擷取到這個屬性的
- Python使用register的方式
- 雖然現在可以把 register 當作裝飾器使用了,但更常見的做法還是把它當作函數使用,用于注冊其他地方定義的類
- 鵝的行為有可能像鴨子
- 即便不注冊,抽象基類也能把一個類識别為虛拟子類
- 比如一個類實作了
方法:__len__()
class Struggle: def __len__(self): return 23
- 然後可以看到這個類是Sized抽象基類的虛拟子類
from collections import abc isinstance(Struggle(), abc.Sized) # True issubclass(Struggle, abc.Sized) # True
- 這是因為
實作了一個特殊的類方法:abc.Sized
__sunclasshook__
-
在白鵝類型中添加了一些鴨子類型的蹤迹。__subclasshook__
- 我們可以使用抽象基類定義正式接口,可以始終使用 isinstance 檢查
- 也可以完全使用不相關的類,隻要實作特定的方法即可(或者做些事情讓
信服)__subclasshook__
- 當然,隻有提供
方法的抽象基類才能這麼做。__subclasshook__
- 在你我自己編寫的抽象基類中實作
方法,可靠性很低__subclasshook__
- 程式員最好讓 Spam 繼承 Tombola,至少也要注冊(Tombola.register(Spam))
- 自己實作的
方法還可以檢查方法簽名和其他特性,但我覺得不值得這麼做__subclasshook__
- 強類型和弱類型
- 如果一門語言很少隐式轉換類型,說明它是強類型語言;如果經常這麼做,說明它是弱類型語言
- Java、C++ 和 Python 是強類型語言
- PHP、JavaScript 和 Perl 是弱類型語言
- 靜态類型和動态類型
- 在編譯時檢查類型的語言是靜态類型語言,在運作時檢查類型的語言是動态類型語言
- 靜态類型需要聲明類型(有些現代語言使用類型推導避免部分類型聲明)
- Fortran 和 Lisp 是最早的兩門語言,現在仍在使用,它們分别是靜态類型語言和動态類型語言
- 小結
- 強類型能及早發現缺陷
- 靜态類型使得一些工具(編譯器和 IDE)便于分析代碼、找出錯誤和提供其他服務(優化、重構,等等)
- 動态類型便于代碼重用,代碼行數更少,而且能讓接口自然成為協定而不提早實行
- Python 是動态強類型語言
- 猴子更新檔
- 猴子更新檔的名聲不太好。如果濫用,會導緻系統難以了解和維護。更新檔通常與目标緊密耦合,是以很脆弱。另一個問題是,打了猴子更新檔的兩個庫可能互相牽絆,因為第二個庫可能撤銷了第一個庫的更新檔。
- 猴子更新檔也有它的作用,例如可以在運作時讓類實作協定。擴充卡設計模式通過實作全新的類解決這種問題。
- Python 不允許為内置類型打猴子更新檔,這一局限能減少外部庫打的更新檔有沖突的機率
- 不把隐喻當作設計範式,而代之以“習慣用法的界面”
- 用isinstance檢查對象的類型,而不是
12. 繼承的優缺點
- 子類化内置類型很麻煩
- 内置類型不會調用使用者定義的類覆寫的特殊方法
- 内置類型的方法不會調用子類覆寫的方法
- 直接子類化内置類型(如 dict、list 或 str)容易出錯,因為内置類型的方法通常會忽略使用者覆寫的方法
- 不要子類化内置類型,使用者自己定義的類應該繼承 collections 子產品中的類,例如UserDict、UserList 和 UserString,這些類做了特殊設計,是以易于擴充
- 如果子類化使用Python 編寫的類,如 UserDict 或 MutableMapping,就不會受此影響
- 多重繼承和方法解析順序
- “菱形問題”:不相關的祖先類實作同名方法引起的沖突
- Python 會按照特定的順序周遊繼承圖,這個順序叫方法解析順序(Method Resolution Order, MRO)
- 直接在類上調用執行個體方法時,必須顯式傳入 self 參數,因為這樣通路的是未綁定方法(unbound method)
- 使用 super() 最安全,也不易過時。調用架構或不受自己控制的類層次結構中的方法時,尤其适合使用 super()
- 使用 super() 調用方法時,會遵守方法解析順序
- 方法解析順序不僅考慮繼承圖,還考慮子類聲明中列出超類的順序,**先聲明的先調用你敢信?**沒事多看看
屬性吧,按這個順序調用。__mro__
- 多重繼承的真實應用(以Tkinter為例)
- Toplevel 是所有圖形類中唯一沒有繼承 Widget 的,因為它是頂層視窗,行為不像小元件,例如不能依附到視窗或窗體上
- Toplevel 繼承自 Wm,後者提供直接通路宿主視窗管理器的函數,例如設定視窗标題和配置視窗邊框
- Widget 直接繼承自 BaseWidget,還繼承了 Pack、Place 和Grid。後三個類是幾何管理器,負責在視窗或窗體中排布小元件。各個類封裝了不同的布局政策和小元件位置 API
- 處理多重繼承
- 下面是避免把類圖攪亂的一些建議
- 把接口繼承和實作繼承區分開
- 繼承接口,建立子類型,實作“是什麼”關系
- 繼承實作,通過重用避免代碼重複
- 使用抽象基類顯式表示接口
- 通過混入重用代碼
- 如果一個類的作用是為多個不相關的子類提供方法實作,進而實作重用,但不展現“是什麼”關系,應該把那個類明确地定義為混入類(mixin class)
- 從概念上講,混入不定義新類型,隻是打包方法,便于重用
-
混入類絕對不能執行個體化,而且具體類不能隻繼承混
入類
- 混入類應該提供某方面的特定行為,隻實作少量關系非常緊密的方法
-
在名稱中明确指明混入
因為在 Python 中沒有把類聲明為混入的正規方式,是以強烈推薦在名稱中加入 …Mixin 字尾
- 抽象基類可以作為混入,反過來則不成立
- 不要子類化多個具體類
- 具體類可以沒有,或最多隻有一個具體超類
- 具體類的超類中除了這一個具體超類之外,其餘的都是抽象基類或混入
- 為使用者提供聚合類
- 如果抽象基類或混入的組合對客戶代碼非常有用,那就提供一個類,使用易于了解的方式把它們結合起來。Grady Booch 把這種類稱為聚合類(aggregate class)
- 例如
類中繼承了多個抽象基類/混入:tkinter.Widget
class Widget(BaseWidget, Pack, Place, Grid):pass
- Widget 類的定義體是空的,但是這個類提供了有用的服務:把四個超類結合在一起,這樣需要建立新小元件的使用者無需記住全部混入,也不用擔心聲明 class 語句時有沒有遵守特定的順序
-
“優先使用對象組合,而不是類繼承”
組合和委托可以代替混入,把行為提供給不同的類,但是不能取代接口繼承去定義類型層次結構
- 依賴、關聯、聚合、組合
- 混入,python多繼承和混入類,混入類就是多重繼承的一種實作技巧,為了防止類的指數增長,為了使具體類具有多個獨立、解耦的功能(個人想法)
- 把接口繼承和實作繼承區分開
- KISS 原則:KISS 原則是使用者體驗的高層境界,簡單地了解這句話,就是要把一個産品做得連白癡都會用,因而也被稱為“懶人原則”。
- Keep it Simple and Stupid
- 内置類型:dict、list 和 str 類型
- 是 Python 的底層基礎,速度必須快,與這些内置類型有關的任何性能問題幾乎都會對其他所有代碼産生重大影響
- CPython 走了捷徑,故意讓内置類型的方法行為不當,即不調用被子類覆寫的方法
- 解決這一困境的可能方式之一是,為這些類型分别提供兩種實作:一種供内部使用,為解釋器做了優化;另一種供外部使用,便于擴充(UserDict、UserList 和 UserString)
- 多重分派:
- 分派就是指根據變量的類型選擇相應的方法,單分派指的是指根據第一個參數類型去選擇方法。
- 函數func()的結果隻跟第一個參數的類型有關,跟後面的參數沒有關系,這就是單分派。
- 使用函數的所有參數,而非隻用第一個,來決定調用哪個方法被稱為多重分派。
- 下面是避免把類圖攪亂的一些建議
13. 正确重載運算符
- 運算符重載基礎
- Python 施加了一些限制,做好了靈活性、可用性和安全性方面的平衡:
- 不能重載内置類型的運算符
- 不能建立運算符,隻能重載現有的
- 某些運算符不能重載——is、and、or 和 not(不過位運算符 &、| 和 ~ 可以)
- Python 施加了一些限制,做好了靈活性、可用性和安全性方面的平衡:
- 一進制運算符
- 常見的運算符和對應的特殊方法:
-
:-
,取負算術運算符__neg__
-
:+
,取正算術運算符__pos__
-
:~
,按位取反__invert__
- 定義為 x = = − ( x + 1 ) ~x == -(x+1) x==−(x+1)
- 如果x是2,那麼~x == -3
-
:abs()
,取絕對值__abs__
-
- x 和 +x 何時不相等
- jupyter和ipython中無法重制例子,但是cmd可以
- Counter的例子
- Counter 相加時,負值和零值計數會從結果中剔除
- 而一進制運算符 + 等同于加上一個空 Counter,是以它産生一個新的 Counter 且僅保留大于零的計數器
- 什麼意思呢,就是說,如果使用Counter對序列進行計數之後,然後給部分key負值為0或者負值,然後在前面使用+算術運算符,就會導緻負值和0對應的
鍵值對剔除。key:value
- 常見的運算符和對應的特殊方法:
- 重載向量加法運算符+
-
和__radd__
中的r:reflected(反射),reverse(反向),兩種皆可,推薦使用反向__rsub__
- r這種特殊的方法,是一種後備機制,如果左操作數沒有實作對應的方法,或者實作了,但是傳回的是
表明它不知道如何處理右操作數,那麼Python會調用r的方法。NotImplemented
-
- 重載标量乘法運算符*
-
:計算标量積(scalar product),結果是一個新Vector執行個體,各個分量都會乘以x,這也叫元素級乘法(elementwise multiplication)Vector([1, 2, 3]) * x
- 在Numpy庫中,點積使用
計算numpy.dot()
- Python3.5起,引入
用作中綴點選運算符@
-
和__mul__
方法__rmul__
- decimal.Decimal 沒有把自己注冊為 numbers.Real 的虛拟子類。是以,Vector 類不會處理decimal.Decimal 數字。
-
- 中綴運算符方法的名稱
運算符 正向方法 反向方法 就地方法 說明 + __add__
__radd__
__iadd__
加法或拼接 - __sub__
__rsub__
__isub__
減法 * __mul__
__rmul__
__imul__
乘法或重複複制 / __truediv__
__rtruediv__
__itruediv__
除法 // __floordiv__
__rfloordiv__
__ifloordiv__
整除 % __mod__
__rmod__
__imod__
取模 divmod() __divmod__
__rdivmod__
__idivmod__
傳回由整除的商和模數組成的元組 **, pow() __pow__
__rpow__
__ipow__
取幂 @ __matmul__
__rmatmul__
__imatmul__
矩陣乘法 & __add__
__radd__
__iadd__
位與 | __or__
__ror__
__ior__
位或 ^ __xor__
__rxor__
__ixor__
位異或 << __lshift__
__rlshift__
__ilshift__
按位左移 >> __rshift__
__rrshift__
__irshift__
按位右移 - 衆多比較運算符
- ==、!=、>、<、>=、<=
- 正向和反向調用使用的是同一系列方法
- 而正向的
方法調用的是反向的__gt__
方法,并把參數對調__lt__
- 對 == 和 != 來說,如果反向調用失敗,Python 會比較對象的 ID,而不抛出 TypeError
- 衆多比較運算符:正向方法傳回
的話,調用反向方法NotImplemented
- 衆多比較運算符
分組 中綴運算符 正向方法調用 反向方法調用 後備機制 相等性 a == b a.__eq__(b)
b.__eq__(a)
傳回 id(a) == id(b)
a != b a.__ne__(b)
b.__ne__(a)
傳回 not(a == b)
排序 a > b a.__gt__(b)
b.__lt__(a)
抛出TypeError a < b a.__lt__(b)
b.__gt__(a)
抛出TypeError a >= b a.__ge__(b)
b.__le__(a)
抛出TypeError a < b a.__le__(b)
b.__ge__(a)
抛出TypeError - 增量指派運算符
- 如果一個類沒有實作上表列出的就地運算符,增量指派運算符隻是文法糖:a += b 的作用與 a = a + b 完全一樣
- 如果實作了就地運算符方法,例如
,計算 a += b 的結果時會調用就地運算符方法。這種運算符的名稱表明,它們會就地修改左操作數,而不會建立新對象作為結果__iadd__
- 不可變類型,一定不能實作就地特殊方法
- 如果操作數的類型不同,我們要檢測出不能處理的操作數。本章使用兩種方式處理這個問題:
- 一種是鴨子類型,直接嘗試執行運算,如果有問題,捕獲 TypeError 異常
- 鴨子類型更靈活,但是顯式檢查更能預知結果
- 另一種是顯式使用 isinstance 測試,
方法就是這麼做的__mul__
- 如果選擇使用 isinstance,要小心,不能測試具體類,而要測試
抽象基類,例如numbers.Real
isinstance(scalar, numbers.Real)
- 這在靈活性和安全性之間做了很好的折中
- 如果選擇使用 isinstance,要小心,不能測試具體類,而要測試
- 一種是鴨子類型,直接嘗試執行運算,如果有問題,捕獲 TypeError 異常
- 在可接受的類型方面,+ 應該比 += 嚴格
- 對序列類型來說,+ 通常要求兩個操作數屬于同一類型
- 而 += 的右操作數往往可以是任何可疊代對象
14. 可疊代的對象、疊代器和生成器
- 疊代器模式(Iterator pattern):疊代是資料處理的基石。掃描記憶體中放不下的資料集時,我們要找到一種惰性擷取資料項的方式,即按需一次擷取一個資料項。
- 疊代器:用于從集合中取出元素
- 生成器:用于“憑空”生成元素
- 在 Python社群中,大多數時候都把疊代器和生成器視作同一概念
- 單詞序列
- 第一版Sentence
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text self.words = RE_WORD.findall(text) def __getitem__(self, index): return self.words[index] def __len__(self): return len(self.words) def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text)
- 序列可以疊代的原因:iter函數,解釋器需要疊代對象 x 時,會自動調用 iter(x)。
- 内置的 iter 函數有以下作用:
- 檢查對象是否實作了
方法,如果實作了就調用它,擷取一個疊代器__iter__
- 如果沒有實作
方法,但是實作了__iter__
方法,Python 會建立一個疊代器,嘗試按順序(從索引 0 開始)擷取元素__getitem__
- 如果嘗試失敗,Python 抛出 TypeError 異常,通常會提示“C object is not iterable”(C 對象不可疊代),其中 C 是目标對象所屬的類
- 檢查對象是否實作了
- 這是鴨子類型(duck typing)的極端形式:不僅要實作特殊的
方法,還要實作__iter__
方法,而且__getitem__
方法的參數是從 0 開始的整數(int),這樣才認為對象是可疊代的__getitem__
- 白鵝類型(goose typing)理論中,可疊代對象的定義簡單一些,不過沒那麼靈活:如果實作了
方法,那麼就認為對象是可疊代的。此時,不需要建立子類,也不用注冊,因為 abc.Iterable 類實作了__iter__
方法__subclasshook__
- 檢查對象 x 能否疊代,最準确的方法是:
- 調用 iter(x) 函數,如果不可疊代,再處理 TypeError 異常。
- 這比使用 isinstance(x, abc.Iterable) 更準确
- 因為 iter(x) 函數會考慮到遺留的
__getitem__
方法,而 abc.Iterable 類則
不考慮
- 第一版Sentence
- 可疊代的對象與疊代器的對比
- 可疊代的對象和疊代器之間的關系:Python 從可疊代的對象中擷取疊代器
- StopIteration 異常表明疊代器到頭了
- 标準的疊代器接口有兩個方法:
-
:傳回下一個可用的元素,如果沒有元素了,抛出 StopIteration異常__next__
-
:傳回 self,以便在應該使用可疊代對象的地方使用疊代器,例如在 for 循環中__iter__
-
- 在
源碼中,根據Iterator(Iterable)
函數識别C是不是Iterator的子類。這種方法是通過在__subclasshook__(cls, C)
中找是否同時存在C.__mro__
和__next__
方法__iter__
- 檢查對象 x 是否為疊代器最好的方式是調用
isinstance(x, abc.Iterator)
- 代器是這樣的對象:
- 實作了無參數的
方法,傳回序列中的下一個元素;__next__
- 如果沒有元素了,那麼抛出 StopIteration 異常
- Python 中的疊代器還實作了
方法,是以疊代器也可以疊代__iter__
- 實作了無參數的
- 典型的疊代器
- 建構可疊代的對象和疊代器時經常會出現錯誤,原因是混淆了二者
- 可疊代的對象有個
方法,每次都執行個體化一個新的疊代器__iter__
- 而疊代器要實作
方法,傳回單個元素,此外還要實作__next__
方法,傳回疊代器本身__iter__
- 可疊代的對象有個
- 疊代器可以疊代,但是可疊代的對象不是疊代器
- 疊代器模式可用來:
- 通路一個聚合對象的内容而無需暴露它的内部表示
- 支援對聚合對象的多種周遊
- 為周遊不同的聚合結構提供一個統一的接口(即支援多态疊代)
- 為了“支援多種周遊”,必須能從同一個可疊代的執行個體中擷取多個獨立的疊代器,而且各個疊代器要能維護自身的内部狀态,是以這一模式正确的實作方式是,每次調用 iter(my_iterable) 都建立一個獨立的疊代器。這就是為什麼這個示例需要定義 SentenceIterator 類
- 可疊代的對象一定不能是自身的疊代器。也就是說,可疊代的對象必須實作
方法,但不能實作__iter__
方法__next__
- 另一方面,疊代器應該一直可以疊代。疊代器的
方法應該傳回自身__iter__
- 第二版Sentence
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text self.words = RE_WORD.findall(text) def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text) def __iter__(self): return SentenceIterator(self.words) class SentenceIterator: def __init__(self, words): self.words = words self.index = 0 def __next__(self): try: word = self.words[self.index] except IndexError: raise StopIteration() self.index += 1 return word def __iter__(self): return self
- 建構可疊代的對象和疊代器時經常會出現錯誤,原因是混淆了二者
- 生成器函數
- 達到 實作相同功能,但符合Python習慣 的方式:用生成器函數代替SentenceIterator 類
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text self.words = RE_WORD.findall(text) def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text) def __iter__(self): for word in self.words: yield word return
- 這個 return 語句不是必要的;這個函數可以直接“落空”,自動傳回。不管有沒有 return 語句,生成器函數都不會抛出 StopIteration 異常,而是在生成完全部值之後會直接退出
- 生成器函數的工作原理
- 隻要Python函數的定義體中有yield關鍵字,該函數就是生成器函數
- 調用生成器函數時,會傳回一個生成器對象
- 也就是說,生成器函數是生成器工廠
- 描述:
- 函數傳回值;
- 調用生成器函數傳回生成器;
- 生成器産出或生成值
- 生成器不會以正常的方式“傳回”值:生成器函數定義體中的 return 語句會觸發生成器對象抛出 StopIteration 異常
- 達到 實作相同功能,但符合Python習慣 的方式:用生成器函數代替SentenceIterator 類
- 惰性實作
- 惰性求值(lazy evaluation)和 及早求值(eager evaluation)是程式設計語言理論方面的技術術語
- re.finditer 函數是 re.findall 函數的惰性版本,傳回的不是清單,而是一個生成器,能節省大量記憶體
- Sentence類第4版:
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text) def __iter__(self): for match in RE_WORD.finditer(self.text): yield match.group()
- 生成器表達式
- 生成器表達式可以了解為清單推導的惰性版本:不會迫切地建構清單,而是傳回一個生成器,按需惰性生成元素
- 如果清單推導是制造清單的工廠,那麼生成器表達式就是制造生成器的工廠
- 生成器表達式會産出生成器
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text) def __iter__(self): return (match.group() for match in RE_WORD.finditer(self.text))
- 生成器表達式是文法糖:完全可以替換成生成器函數,不過有時使用生成器表達式更便利
- 何時使用生成器表達式
- 如果函數或構造方法隻有一個參數,傳入生成器表達式時不用寫一對調用函數的括号,再寫一對括号圍住生成器表達式,隻寫一對括号就行了
- 如果生成器表達式後面還有其他參數,那麼必須使用括号圍住,否則會抛出 SyntaxError 異常
- 等差數列生成器
- 類生成器
class ArithmeticProgression: def __init__(self, begin, step, end=None): self.begin = begin self.step = step self.end = end def __iter__(self): result = type(self.begin + self.step)(self.begin) forever = self.end is None index = 0 while forever or result < self.end: yield result index += 1 result = self.begin + self.step * index
- 函數生成器
def aritpro_gen(begin, step, end=None): result = type(begin + step)(begin) forever = end is None index = 0 while forever or result < end: yield result index += 1 result = begin + step * index
- 使用itertools子產品生成等差數列
import itertools gen = itertools.count(1, .5) next(gen)
-
itertools.takewhile
- 它會生成一個使用另一個生成器的生成器
- 在指定的條件計算結果為 False 時停止
- 可以把這兩個函數結合在一起使用
- 生成器工廠,傳回的是一個生成器
import itertools def aritpro_gen(begin, step, end=None): first = type(begin + step)(begin) ap_gen = itertools.count(first, step) if end is not None: ap_gen = itertools.takewhile(lambda n: n < end, ap_gen) return ap_gen
- 類生成器
- 标準庫中的生成器函數,參考連結
-
下面大多數函數都接受一個斷言參數(predicate),這個參數是個布爾函數,有一個參數,會應用到輸入中的每個元素上,用于判斷元素是否包含在輸出中。
用于過濾的生成器函數
子產品 函數 說明 itertools compress(it, selector_it) 并行處理兩個可疊代的對象;如果 selector_it 中的元素是真值,産出 it 中對應的元素 itertools dropwhile(predicate, it) 處理 it,跳過 predicate 的計算結果為真值的元素,然後産出剩下的各個元素(不再進一步檢查) (内置) filter(predicate, it) 把 it 中的各個元素傳給 predicate,如果predicate(item) 傳回真值,那麼産出對應的元素;如果 predicate 是 None,那麼隻産出真值元素 itertools filterfalse(predicate, it) 與 filter 函數的作用類似,不過 predicate 的邏輯是相反的:predicate 傳回假值時産出對應的元素 itertools islice(it, stop) 或islice(it, start, stop, step=1) 産出 it 的切片,作用類似于 s[:stop] 或s[start:stop:step],不過 it 可以是任何可疊代的對象,而且這個函數實作的是惰性操作 itertools takewhile(predicate, it) predicate 傳回真值時産出對應的元素,然後立即停止,不再繼續檢查 - 上述函數測試
-
按照真值表篩選元素compress(it, selector_it)
import itertools temp = [1, 0, -1, 2, 4, 9] temp_select = [x > 2 for x in temp] it = itertools.compress(temp, temp_select) list(it) # [4, 9]
-
按照真值函數丢棄掉清單和疊代器前面的元素dropwhile(predicate, it)
import itertools x = itertools.dropwhile(lambda e: e < 5, range(10)) list(x) # [5, 6, 7, 8, 9]
-
過濾函數為True的元素filter(predicate, it)
x = filter(lambda e: e < 5, range(10)) list(x) # [0, 1, 2, 3, 4]
-
保留對應真值為False的元素filterfalse(predicate, it)
import itertools x = itertools.filterfalse(lambda e: e < 5, (1, 5, 3, 6, 9, 4)) list(x) # [5, 6, 9]
-
對疊代器進行切片islice(it, stop) 或 islice(it, start, stop, step=1)
import itertools x = itertools.islice(range(10), 0, 9, 2) list(x) # [0, 2, 4, 6, 8]
-
與dropwhile相反,保留元素直至真值函數值為假(有順序,一旦為假,後面的就不管了)takewhile(predicate, it)
import itertools x = itertools.takewhile(lambda x: x.lower() in 'aeiou', 'Aardvark') list(x) # ['A', 'a']
-
- 下一組是用于映射(map)的生成器函數:在輸入的單個可疊代對象(map 和starmap 函數處理多個可疊代的對象)中的各個元素上做計算,然後傳回結果
- 生成器函數會從輸入的可疊代對象中的各個元素中産出一個元素
- 如果輸入來自多個可疊代的對象,第一個可疊代的對象到頭後就停止輸出
- 用于映射的生成器函數
子產品 函數 說明 itertools accumulate(it, [func]) 産出累積的總和;如果提供了 func,那麼把前兩個元素傳給它,然後把計算結果和下一個元素傳給它,以此類推,最後産出結果 (内置) enumerate(iterable, start=0) 産出由兩個元素組成的元組,結構是 (index, item),其中 index 從 start 開始計數,item 則從 iterable 中擷取 (内置) map(func, it1, [it2, …, itN]) 把 it 中的各個元素傳給func,産出結果;如果傳入 N 個可疊代的對象,那麼 func 必須能接受 N 個參 數,而且要并行處理各個可疊代的對象 itertools starmap(func, it) 把 it 中的各個元素傳給 func,産出結果;輸入的 可疊代對象應該産出可疊代的元素 iit,然後以 func(*iit) 這種形式調用 func
- 上述函數測試
-
簡單來說就是累加accumulate(it, [func])
import itertools sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] print(list(itertools.accumulate(sample))) # [5, 9, 11, 19, 26, 32, 35, 35, 44, 45] print(list(itertools.accumulate(sample, min))) # [5, 4, 2, 2, 2, 2, 2, 0, 0, 0] print(list(itertools.accumulate(sample, max))) # [5, 5, 5, 8, 8, 8, 8, 8, 9, 9] import operator print(list(itertools.accumulate(sample, operator.mul))) # [5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0] print(list(itertools.accumulate(range(1, 11), operator.mul))) # [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]
-
生成編号enumerate(iterable, start=0)
print(list(enumerate('albatroz', 1))) # [(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]
-
map(func, it1, [it2, ..., itN])
import itertools print(list(map(operator.mul, range(11), range(1, 12, 1)))) # [0, 2, 6, 12, 20, 30, 42, 56, 72, 90, 110]
import itertools print(list(map(lambda a,b : (a, b), range(11), [2, 4, 8]))) # [(0, 2), (1, 4), (2, 8)]
-
類似map,starmap(func, it)
子產品的itertools
函數實際上是starmap()
函數的map()
版本,參考連結*a
import itertools print(list(itertools.starmap(operator.mul, enumerate('albatroz', 1)))) # ['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz'] sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] print(list(itertools.starmap(lambda a, b: b / a, enumerate(itertools.accumulate(sample), 1)))) # [5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375, 4.888888888888889, 4.5]
-
-
這一組是用于合并的生成器函數,這些函數都從輸入的多個可疊代對象中産出元素。chain 和 chain.from_iterable 按順序(一個接一個)處理輸入的可疊代對象,而 product、zip 和 zip_longest 并行處理輸入的各個可疊代對象
合并多個可疊代對象的生成器函數
子產品 函數 說明 itertools chain(it1, …, itN) 先産出 it1 中的所有元素,然後産出 it2 中的所有元素,以此類推,無縫連接配接在一起 itertools chain.from_iterable(it) 産出 it 生成的各個可疊代對象中的元素,一個接一個,無縫連接配接在一起;it 應該産出可疊代的元素,例如可疊代的對象清單 itertools product(it1, …, itN, repeat=1) 計算笛卡兒積:從輸入的各個可疊代對象中擷取元素,合并成由 N 個元素組成的元組,與嵌套的 for 循環效果一樣;repeat 指明重複處理 (内置) zip(it1, …, itN) 并行從輸入的各個可疊代對象中擷取元素,産出由 N 個元素組成的元組,隻要有一個可疊代的對象到頭了,就默默地停止 itertools zip_longest(it1, …, itN, fillvalue=None) 并行從輸入的各個可疊代對象中擷取元素,産出由 N 個元素組成的元組,等到最長的可疊代對象到頭後才停止,空缺的值使用 fillvalue 填充 - 上述函數測試
-
chain(it1, ..., itN)
list(itertools.chain('ABC', range(2))) # ['A', 'B', 'C', 0, 1]
-
chain.from_iterable 函數從可疊代的對象中擷取每個元素,然後按順序把元素連接配接起來,前提是各個元素本身也是可疊代的對象chain.from_iterable(it)
list(itertools.chain(enumerate('ABC'))) # [(0, 'A'), (1, 'B'), (2, 'C')]
list(itertools.chain.from_iterable(enumerate('ABC'))) # [0, 'A', 1, 'B', 2, 'C']
-
和zip(it1, ..., itN)
zip_longest(it1, ..., itN, fillvalue=None)
list(zip('ABC', range(5))) # [('A', 0), ('B', 1), ('C', 2)]
list(itertools.zip_longest('ABC', range(5))) # [('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
list(itertools.zip_longest('ABC', range(6), fillvalue='?')) # [('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
-
計算笛卡兒積的惰性方式product(it1, ..., itN, repeat=1)
list(itertools.product('ABC', range(2))) # [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)] print(list(itertools.product('AK', suits))) # [('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'), ('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')] list(itertools.product('ABC')) # [('A',), ('B',), ('C',)] print(list(itertools.product('ABC', repeat=2))) # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] print(list(itertools.product('AB', repeat=3))) # [('A', 'A', 'A'), ('A', 'A', 'B'), ('A', 'B', 'A'), ('A', 'B', 'B'), ('B', 'A', 'A'), ('B', 'A', 'B'), ('B', 'B', 'A'), ('B', 'B', 'B')] print(list(itertools.product('AB', range(2), repeat=2))) # [('A', 0, 'A', 0), ('A', 0, 'A', 1), ('A', 0, 'B', 0), ('A', 0, 'B', 1), ('A', 1, 'A', 0), ('A', 1, 'A', 1), ('A', 1, 'B', 0), ('A', 1, 'B', 1), ('B', 0, 'A', 0), ('B', 0, 'A', 1), ('B', 0, 'B', 0), ('B', 0, 'B', 1), ('B', 1, 'A', 0), ('B', 1, 'A', 1), ('B', 1, 'B', 0), ('B', 1, 'B', 1)]
-
-
有些生成器函數會從一個元素中産出多個值,擴充輸入的可疊代對象
把輸入的各個元素擴充成多個輸出元素的生成器函數
子產品 函數 說明 itertools combinations(it, out_len) 把 it 産出的 out_len 個元素組合在一起,然後産出 itertools combinations_with_replacement(it, out_len) 把 it 産出的 out_len 個元素組合在一起,然後産出,包含相同元素的組合 itertools count(start=0, step=1) 從 start 開始不斷産出數字,按 step 指定的步幅增加 itertools cycle(it) 從 it 中産出各個元素,存儲各個元素的副本,然後按順序重複不斷地産出各個元素 itertools permutations(it, out_len=None) 把 out_len 個 it 産出的元素排列在一起,然後産出這些排列;out_len的預設值等于 len(list(it)) itertools repeat(item, [times]) 重複不斷地産出指定的元素,除非提供 times,指定次數 - 上述函數測試
-
count(start=0, step=1)
ct = itertools.count() next(ct), next(ct), next(ct) # (0, 1, 2) list(itertools.islice(itertools.count(1, .3), 3)) # [1, 1.3, 1.6]
-
cycle(it)
cy = itertools.cycle('ABC') list(itertools.islice(cy, 7)) # ['A', 'B', 'C', 'A', 'B', 'C', 'A']
-
repeat(item, [times])
repeat 函數的常見用途:為 map 函數提供固定參數,這裡提供的是
乘數 5
rp = itertools.repeat(7) next(rp), next(rp), next(rp) # (7, 7, 7) list(itertools.repeat(8, 4)) # [8, 8, 8, 8] list(map(operator.mul, range(11), itertools.repeat(5))) # [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
-
combinations(it, out_len)
- combinations、combinations_with_replacement 和 permutations 生成器函數,連同 product 函數,稱為組合學生成器(combinatoric generator)
- itertools.product 函數和其餘的組合學函數有緊密的聯系
# 兩兩組合,不包括自己,順序無關,相當于排列組合中的組合 list(itertools.combinations('ABC', 2)) # [('A', 'B'), ('A', 'C'), ('B', 'C')] # 兩兩組合,包括自己,順序無關 list(itertools.combinations_with_replacement('ABC', 2)) # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] # 兩兩組合,不包括自己,順序有關 list(itertools.permutations('ABC', 2)) # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] # 兩兩組合,笛卡爾積,包括自己,順序有關 print(list(itertools.product('ABC', repeat=2))) # [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
-
-
最後一組生成器函數用于産出輸入的可疊代對象中的全部元素,不過會以某種方式重新排列
用于重新排列元素的生成器函數
子產品 函數 說明 itertools groupby(it, key=None) 産出由兩個元素組成的元素,形式為 (key, group),其中 key 是分組标準,group 是生成器,用于産出分組裡的元素 (内置) reversed(seq) 從後向前,倒序産出 seq 中的元素;seq 必須是序列,或者是實作了
特殊方法的對象__reversed__
itertools tee(it, n=2) 産出一個由 n 個生成器組成的元組,每個生成器用于單獨産出輸入的可疊代對象中的元素 - 上述函數測試
-
groupby(it, key=None)
假定輸入的可疊代對象要使用分組标準排序;即使不排序,至少也要使用指定的标準分組各個元素itertools.groupby
import itertools print(list(itertools.groupby('LLLLAAGGG'))) # [('L', <itertools._grouper object at 0x0000018A1AF32948>), ('A', <itertools._grouper object at 0x0000018A1AF32248>), ('G', <itertools._grouper object at 0x0000018A1AF32708>)] for char, group in itertools.groupby('LLLLAAAGG'): print(char, '->', list(group)) # L -> ['L', 'L', 'L', 'L'] # A -> ['A', 'A', 'A'] # G -> ['G', 'G'] animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', 'bat', 'dolphin', 'shark', 'lion'] animals.sort(key=len) animals # ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin'] for length, group in itertools.groupby(animals, len): print(length, '->', list(group)) """ 3 -> ['rat', 'bat'] 4 -> ['duck', 'bear', 'lion'] 5 -> ['eagle', 'shark'] 7 -> ['giraffe', 'dolphin'] """ for length, group in itertools.groupby(reversed(animals), len): print(length, '->', list(group)) """ 7 -> ['dolphin', 'giraffe'] 5 -> ['shark', 'eagle'] 4 -> ['lion', 'bear', 'duck'] 3 -> ['bat', 'rat'] """
-
輸入的一個可疊代對象中産出多個生成器,每個生成器都可以産出輸入的各個元素tee(it, n=2)
list(itertools.tee('ABC')) # [<itertools._tee at 0x18a1b6ac808>, <itertools._tee at 0x18a1b6ac688>] g1, g2 = itertools.tee('ABC') list(g2), list(g2) # (['A', 'B', 'C'], ['A', 'B', 'C']) list(zip(*itertools.tee('ABC'))) # [('A', 'A'), ('B', 'B'), ('C', 'C')]
-
-
- Python3.3中新出現的文法:yield from
- 下面兩個代碼等價
def chain(*iterrables): print(iterrables) for it in iterrables: for i in it: yield i
def chain(*iterables): for i in iterables: yield from i
- 可以看出,yield from i 完全代替了内層的 for 循環
- 除了代替循環之外,yield from 還會建立通道,把内層生成器直接與外層生成器的用戶端聯系起來。把生成器當成協程使用時,這個通道特别重要,不僅能為用戶端代碼生成值,還能使用用戶端代碼提供的值
- 下面兩個代碼等價
- 可疊代的歸約函數
- 下述函數都接受一個可疊代的對象,然後傳回單個結果。這些函數叫“歸約”函數、“合攏”函數或“累加”函數
- 這裡列出的每個内置函數都可以使用 functools.reduce 函數實作,内置是因為使用它們便于解決常見的問題
- 對 all 和 any 函數來說,有一項重要的優化措施是 reduce 函數做不到的:這兩個函數會短路(即一旦确定了結果就立即停止使用疊代器)
- 讀取疊代器,傳回單個值的内置函數
- sorted 和這些歸約函數隻能處理最終會停止的可疊代對象。否則,這些函數會一直收集元素,永遠無法傳回結果
子產品 函數 說明 (内置) all(it) it 中的所有元素都為真值時傳回 True,否則傳回 False;all([]) 傳回 True (内置) any(it) 隻要 it 中有元素為真值就傳回 True,否則傳回 False;any([]) 傳回 False (内置) max(it, [key=,][default=])
傳回 it 中值最大的元素;key 是排序函數,與 sorted 函數中的一樣;如果可疊代的對象為空,傳回 default (内置) min(it, [key=,][default=])
傳回 it 中值最小的元素;key 是排序函數,與 sorted 函數中的一樣;如果可疊代的對象為空,傳回 default functools reduce(func,it,[initial]) 把前兩個元素傳給 func,然後把計算結果和第三個元素傳給 func,以此類推,傳回最後的結果;如果提供了initial,把它當作第一個元素傳入 (内置) sum(it,start=0) it 中所有元素的總和,如果提供可選的 start,會把它加上(計算浮點數的加法時,可以使用 math.fsum 函數提高精度)
- 深入分析iter函數
- iter 函數還有一個鮮為人知的用法
- 傳入兩個參數,使用正常的函數或任何可調用的對象建立疊代器
- 這樣使用時,第一個參數必須是可調用的對象,用于不斷調用(沒有參數),産出各個值
- 第二個值是哨符,這是個标記值,當可調用的對象傳回這個值時,觸發疊代器抛出 StopIteration 異常,而不産出哨符
- 如何使用 iter 函數擲骰子,直到擲出 1 點為止
from random import randint def d6(): return randint(1, 6) d6_iter = iter(d6, 1) for roll in d6_iter: print(roll)
- 逐行讀取檔案,直到遇到空行或者到達檔案末尾為止:
with open('mydate.txt') as fp: for line in iter(fp.readline, '\n'): process_line(line)
- iter 函數還有一個鮮為人知的用法
- 案例分析:在資料庫轉換工具中使用生成器
- 把生成器當成協程
- 與
方法一樣,.__next__()
方法緻使生成器前進到下一個yield 語句.send()
-
方法還允許使用生成器的客戶把資料發給自己,即不管傳給.send()
方法什麼參數,那個參數都會成為生成器函數定義體中對應的 yield 表達式的值.send()
- 也就是說,
方法允許在客戶代碼和生成器之間雙向交換資料.send()
- 而
方法隻允許客戶從生成器中擷取資料.__next__()
- 像這樣使用的話,生成器就變身為協程
- 雖然在協程中會使用 yield 産出值,但這與疊代無關
- 與
- 雜談
- grok 的意思不僅是學會了新知識,還要充分吸收知識,做到“人劍合一”
- 設計模式在各種程式設計語言中使用的方式并不相同
- yield 關鍵字隻能把最近的外層函數變成生成器函數
- 雖然生成器函數看起來像函數,可是我們不能通過簡單的函數調用把職責委托給另一個生成器函數
- Python 新引入的 yield from 句法允許生成器或協程把工作委托給第三方完成,這樣就無需嵌套 for 循環作為變通了
def f(): def do_yield(n): yield n x=0 while True: x += 1 yield from do_yield(x)
- 在協程中,yield 碰巧(通常)出現在指派語句的右手邊,因為 yield 用于接收客戶傳給 .send() 方法的參數
- 盡管有一些相同之處,但是生成器和協程基本上是兩個不同的概念
- 生成器與疊代器的語義對比
- 第一方面是接口
- Python 的疊代器協定定義了兩個方法:
和__next__
__iter__
- 生成器對象實作了這兩個方法,是以從這方面來看,所有生成器都是疊代器
- 由此可以得知,内置的 enumerate() 函數建立的對象是疊代器
- Python 的疊代器協定定義了兩個方法:
- 第二方面是實作方式
- 生成器這種 Python 語言結構可以使用兩種方式編寫:含有 yield 關鍵字的函數,或者生成器表達式
- 調用生成器函數或者執行生成器表達式得到的生成器對象屬于語言内部的 GeneratorType 類型
- 從這方面來看,所有生成器都是疊代器,因為 GeneratorType 類型的執行個體實作了疊代器接口
- 我們可以編寫不是生成器的疊代器,方法是實作經典的疊代器模式
- 從這方面來看,enumerate 對象不是生成器
- types.GeneratorType 類型的定義:生成器-疊代器對象的類型,調用生成器函數時生成
- 第三方面是概念
- 在典型的疊代器設計模式中,疊代器用于周遊集合,從中産出元素
- 不管典型的疊代器中有多少邏輯,都是從現有的資料源中讀取值
- 疊代器不能修改從資料源中讀取的值,隻能原封不動地産出值
- 生成器可能無需周遊集合就能生成值
- 即便依附了集合,生成器不僅能産出集合中的元素,還可能會産出派生自元素的其他值
- 在典型的疊代器設計模式中,疊代器用于周遊集合,從中産出元素
- 第一方面是接口
15. 上下文管理器和else塊
- with 語句會設定一個臨時的上下文,交給上下文管理器對象控制,并且負責清理上下文
- 這麼做能避免錯誤并減少樣闆代碼,是以 API 更安全,而且更易于使用
- 了自動關閉檔案之外,with 塊還有很多用途
- 先做這個,再做那個:if語句之外的else塊
- else 子句不僅能在if 語句中使用,還能在 for、while 和 try 語句中使用
- else-for:僅當 for 循環運作完畢時(即 for 循環沒有被 break 語句中止)才運作 else 塊
- else-while:僅當 while 循環因為條件為假值而退出時(即 while 循環沒有被 break 語句中止)才運作 else 塊
- try-else:僅當 try 塊中沒有異常抛出時才運作 else 塊,else 子句抛出的異常不會由前面的 except 子句處理
- 在所有情況下,如果異常或者 return、break 或 continue 語句導緻控制權跳到了複合語句的主塊之外,else 子句也會被跳過
- 這裡在嘗試了解的時候,可以把else了解為then,即,嘗試運作這個,然後做那個,意思就是說,在運作for/while/try沒有發生問題時,就運作else
- 上下文管理器和with塊
- with 語句的目的是簡化 try/finally 模式
- 上下文管理器協定包含
和__enter__
兩個方法__exit__
- with 語句開始運作時,會在上下文管理器對象上調用
方法__enter__
- with 語句運作結束後,會在上下文管理器對象上調用
方法__exit__
- with 語句開始運作時,會在上下文管理器對象上調用
- 執行 with 後面的表達式得到的結果是上下文管理器對象
- 不過,把值綁定到目标變量上(as 子句)是在上下文管理器對象上調用
方法的結果__enter__
- 解釋器調用
方法時,除了隐式的 self 之外,不會傳入任何參數__enter__
- 傳給
方法的三個參數列舉如下:__exit__
-
:異常類(例如 ZeroDivisionError)exc_type
-
:異常執行個體。有時會有參數傳給異常構造方法,例如錯誤消息,這些參數可以使用 exc_value.args 擷取exc_value
-
:traceback 對象traceback
-
- 例子
class LookingGlass: def __enter__(self): import sys self.original_write = sys.stdout.write sys.stdout.write = self.reverse_write return 'JABBERWOCKY' def reverse_write(self, text): self.original_write(text[::-1]) def __exit__(self, exc_type, exc_value, traceback): import sys sys.stdout.write = self.original_write if exc_type is ZeroDivisionError: print(exc_type, exc_value, traceback) print('Please DO NOT divide by zero') return True
- contextlib子產品中的實用工具
- closing:如果對象提供了 close() 方法,但沒有實作
協定,那麼可以使用這個函數建構上下文管理器__enter__/__exit__
- suppress:建構臨時忽略指定異常的上下文管理器
-
@contextmanager(用得最多):這個裝飾器把簡單的生成器函數變成上下文管理器,這樣就不用建立類去實作管理器協定了
**注意:**它與疊代無關,卻要使用 yield 語句
- ContextDecorator:這是個基類,用于定義基于類的上下文管理器。這種上下文管理器也能用于裝飾函數,在受管理的上下文中運作整個函數
- ExitStack:這個上下文管理器能進入多個上下文管理器
- with 塊結束時,ExitStack 按照後進先出的順序調用棧中各個上下文管理器的
方法__exit__
- 如果事先不知道 with 塊要進入多少個上下文管理器,可以使用這個類
- with 塊結束時,ExitStack 按照後進先出的順序調用棧中各個上下文管理器的
- closing:如果對象提供了 close() 方法,但沒有實作
- 使用@contextmanager
- @contextmanager 裝飾器能減少建立上下文管理器的樣闆代碼量
- 隻需實作有一個 yield 語句的生成器,生成想讓
方法傳回的值__enter__
- 在使用 @contextmanager 裝飾的生成器中,yield 語句的作用是把函數的定義體分成兩部分:
- yield 語句前面的所有代碼在 with 塊開始時(即解釋器調用
方法時)執行__enter__
- yield 語句後面的代碼在 with 塊結束時(即調用
方法時)執行__exit__
- yield 語句前面的所有代碼在 with 塊開始時(即解釋器調用
- 使用生成器實作的上下文管理器
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write yield 'JABBERWOCKY' sys.stdout.write = original_write
- 其實,contextlib.contextmanager 裝飾器會把函數包裝成實作
和__enter__
方法的類;類的名稱是 _GeneratorContextManager__exit__
- 這個類的
方法有如下作用:__enter__
- 調用生成器函數,儲存生成器對象(這裡把它稱為 gen)
- 調用 next(gen),執行到 yield 關鍵字所在的位置
- 傳回 next(gen) 産出的值,以便把産出的值綁定到 with/as 語句中的目标變量上
- with 塊終止時,
方法會做以下幾件事__exit__
- 檢查有沒有把異常傳給 exc_type;如果有,調用 gen.throw(exception),在生成器函數定義體中包含 yield 關鍵字的那一行抛出異常
- 否則,調用 next(gen),繼續執行生成器函數定義體中 yield 語句之後的代碼
- 處理異常的代碼:
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = '' try: yield 'JABBERWOCKY' except Exception: msg = '出錯啦' finally: sys.stdout.write = original_write if msg: print(msg)
- 使用 @contextmanager 裝飾器時,要把 yield 語句放在try/finally 語句中(或者放在 with 語句中),這是無法避免的,因為我們永遠不知道上下文管理器的使用者會在 with 塊中做什麼
- 用于原地重寫檔案的上下文管理器
- 在 @contextmanager 裝飾器裝飾的生成器中,yield 與疊代沒有任何關系。在本節所舉的示例中,生成器函數的作用更像是協程:執行到某一點時暫停,讓客戶代碼運作,直到客戶讓協程繼續做事
- with 不僅能管理資源,還能用于去掉正常的設定和清理代碼,或者在另一個過程前後執行的操作
- @contextmanager 裝飾器優雅且實用,把三個不同的 Python 特性結合到了一起:函數裝飾器、生成器和 with 語句
16. 協程
- 字典為動詞“to yield”給出了兩個釋義:産出和讓步
- 對于 Python 生成器中的 yield 來說,這兩個含義都成立
- yield item 這行代碼會産出一個值,提供給 next(…) 的調用方;此外,還會作出讓步,暫停執行生成器,讓調用方繼續工作,直到需要使用另一個值時再調用next()
- 生成器如何進化成協程
- 協程是指一個過程,這個過程與調用方協作,産出由調用方提供的值
-
:生成器的調用方可以使用 .send(…) 方法發送資料,發送的資料會成為生成器函數中 yield 表達式的值.send(...)
-
:讓調用方抛出異常,在生成器中處理.throw(...)
-
:終止生成器.close()
- 用作協程的生成器的基本行為
- 協程使用生成器函數定義:定義體中有 yield 關鍵字
- 協程可以身處四個狀态中的一個
- 目前狀态可以使用
函數确定inspect.getgeneratorstate(...)
- GEN_CREATED:等待開始執行
- GEN_RUNNING:解釋器正在執行
- GEN_SUSPENDED:在 yield 表達式處暫停
- GEN_CLOSED:執行結束
- 僅當協程處于暫停狀态時才能調用 send 方法
- 如果協程還沒激活(即,狀态是 ‘GEN_CREATED’)
- 始終要調用
激活協程next(my_coro)
- 也可以調用
,效果一樣my_coro.send(None)
- 始終要調用
- 我對yield在協程中使用的了解
def simple_coro2(a): print('-> Started: a =', a) b = yield a print('-> Received: b =', b) c = yield a + b print('-> Received: c =', c)
- 拿這個例子為例
-
:也就是說,程式運作到這一行的時候,先執行b = yield a
,相當于yield a
,前面的return a
是等待指派b = yield
- 然後使用
,就是給b指派了,這一行才算運作結束my_cc.send(28)
- 是以這一行的運作,夾在了兩個
中next(my_cc)
- 示例:使用協程計算移動平均值
def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count
- 使用協程之前必須預激
- 預激協程的裝飾器
- 為了簡化協程的用法,有時會使用一個預激裝飾器
from functools import wraps def coroutine(func): """裝飾器:向前執行到第一個yield表達式,預激func""" @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer
- 需要在協程的函數前面加這個裝飾器
@coroutine def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total/count
- 這樣該協程的初始化的狀态就是 GEN_SUSPENDED
from inspect import getgeneratorstate getgeneratorstate(coro_avg) # 'GEN_SUSPENDED'
- 很多架構都提供了處理協程的特殊裝飾器
- 不是所有裝飾器都用于預激協程
- 有些會提供其他服務,例如勾入事件循環
- 比如說,異步網絡庫 Tornado 提供了 tornado.gen 裝飾器
- 使用 yield from 句法調用協程時,會自動預激
- Python 3.4 标準庫裡的 asyncio.coroutine 裝飾器不會預激協程,是以能相容 yield from 句法
- 為了簡化協程的用法,有時會使用一個預激裝飾器
- 終止協程和異常處理
- 協程中未處理的異常會向上冒泡,傳給 next 函數或 send 方法的調用方(即觸發協程的對象)
- 終止協程的一種方式:發送某個哨符值,讓協程退出
- 内置的 None 和 Ellipsis 等常量經常用作哨符值
- 客戶代碼可以在生成器對象上調用兩個方法,顯式地把異常發給協程
- generator.throw(exc_type[, exc_value[, traceback]]):緻使生成器在暫停的 yield 表達式處抛出指定的異常
- generator.close():緻使生成器在暫停的 yield 表達式處抛出 GeneratorExit 異常
- 學習在協程中處理異常的測試代碼
class DemoException(Exception): """為這次示範定義的異常類型""" def demo_exc_handling(): print('-> coroutine started') while True: try: x = yield except DemoException: print('*** DemoException handled. Continuing...') else: print('-> coroutine received: {!r}'.format(x)) raise RuntimeError('This line should never run.')
- 最後一行代碼不會執行,因為隻有未處理的異常才會中止那個無限循環,而一旦出現未處理的異常,協程會立即終止
- 激活和關閉 demo_exc_handling,沒有異常
- 把 DemoException 異常傳入 demo_exc_handling 不會導緻協程中止
- 如果無法處理傳入的異常,協程會終止
- 使用 try/finally 塊在協程終止時執行操作
class DemoException(Exception): """為這次示範定義的異常類型""" def demo_exc_handling(): print('-> coroutine started') try: while True: try: x = yield except DemoException: print('*** DemoException handled. Continuing...') else: print('-> coroutine received: {!r}'.format(x)) finally: print('-> coroutine ending...') raise RuntimeError('This line should never run.')
- Python 3.3 引入 yield from 結構的主要原因
- 與把異常傳入嵌套的協程有關
- 讓協程更友善地傳回值
- 讓協程傳回值
- 定義一個求平均值的協程,讓它傳回一個結果
from collections import namedtuple Result = namedtuple('Result', 'count average') def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total/count return Result(count, average)
- 最後發送None的時候,協程結束,傳回結果。抛出異常StopIteration,并将return的值儲存到異常對象的value屬性中
- 如何擷取協程的傳回值
try: coro_avg.send(None) except StopIteration as exc: result = exc.value result # Result(count=3, average=15.5)
- yield from 結構會在内部自動捕獲 StopIteration 異常
- 定義一個求平均值的協程,讓它傳回一個結果
- 使用yield from
- yield from 可用于簡化 for 循環中的 yield 表達式
- 下述:
def gen(): for c in 'AB': yield c for i in range(1, 3): yield i
- 可改寫為:
def gen(): yield from 'AB' yield from range(1, 3)
- 下述:
- 相關術語
- 委派生成器:包含
表達式的生成器函數yield from <iterable>
- 子生成器:從 yield from 表達式中
部分擷取的生成器<iterable>
- 調用方:調用委派生成器的用戶端代碼
- 委派生成器:包含
- 委派生成器在 yield from 表達式處暫停時,調用方可以直接把資料發給子生成器,子生成器再把産出的值發給調用方
- 子生成器傳回之後,解釋器會抛出 StopIteration 異常,并把傳回值附加到異常對象上,此時委派生成器會恢複
- 如果子生成器不終止,委派生成器會在 yield from 表達式處永遠暫停
from collections import namedtuple Result = namedtuple('Result', 'count average') # 子生成器 def averager(): total = 0.0 count = 0 average = None while True: term = yield if term is None: break total += term count += 1 average = total/count return Result(count, average) # 委派生成器 def grouper(results, key): while True: results[key] = yield from averager() # 用戶端代碼,即調用方 def main(data): results = {} for key, values in data.items(): group = grouper(results, key) next(group) for value in values: group.send(value) # 重要 group.send(None) report(results) # 輸出報告 def report(results): for key, result in sorted(results.items()): group, unit = key.split(';') print('{:2}{:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit)) data = { 'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], 'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], } if __name__ == '__main__': main(data)
- 委派生成器相當于管道,是以可以把任意數量個委派生成器連接配接在一起
- 一個委派生成器使用 yield from 調用一個子生成器,而那個子生成器本身也是委派生成器,使用 yield from 調用另一個子生成器,以此類推
- 最終,這個鍊條要以一個隻使用 yield 表達式的簡單生成器結束;不過,也能以任何可疊代的對象結束
- 任何 yield from 鍊條都必須由客戶驅動,在最外層委派生成器上調用 next(…) 函數或 .send(…) 方法
- yield from 可用于簡化 for 循環中的 yield 表達式
- yield from 的意義(6點 yield from 的行為)
- 子生成器産出的值都直接傳給委派生成器的調用方(即用戶端代碼)
- 使用 send() 方法發給委派生成器的值都直接傳給子生成器。如果發送的值是 None,那麼會調用子生成器的
方法。如果發送的值不是 None,那麼會調用子生成器的 send() 方法。如果調用的方法抛出 StopIteration 異常,那麼委派生成器恢複運作。任何其他異常都會向上冒泡,傳給委派生成器__next__()
- 生成器退出時,生成器(或子生成器)中的 return expr 表達式會觸發 StopIteration(expr) 異常抛出
- yield from 表達式的值是子生成器終止時傳給 StopIteration 異常的第一個參數
yield from 結構的另外兩個特性與異常和終止有關
- 傳入委派生成器的異常,除了 GeneratorExit 之外都傳給子生成器的 throw() 方法。如果調用 throw() 方法時抛出 StopIteration 異常,委派生成器恢複運作。StopIteration 之外的異常會向上冒泡,傳給委派生成器
-
如果把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用 close() 方法,那麼在子生成器上調用 close() 方法,如果它有的話。
如果調用 close() 方法導緻異常抛出,那麼異常會向上冒泡,傳給委派生成器;否則,委派生成器抛出 GeneratorExit 異常
- 通過閱讀 yield from 的僞代碼,我們可以看到代碼裡已經 預激了子生成器,這說明,用于自動預激的裝飾器與 yield from 不相容
- 使用案例:使用協程做離散時間仿真
- 在計算機科學領域,仿真是協程的經典應用
- 通過仿真系統能說明如何使用協程代替線程實作并發的活動
- 離散事件仿真:Discrete Event Simulation,DES,是一種把系統模組化成一系列事件的仿真類型
- 為了實作連續仿真,在多個線程中處理實時并行的操作更自然。而協程恰好為實作離散事件仿真提供了合理的抽象
- 一個示例:說明如何在一個主循環中處理事件,以及如何通過發送資料驅動協程
sim = Simulator(taxis) sim.run(end_time)
import queue class Simulator: def __init__(self, procs_map): self.events = queue.PriorityQueue() self.procs = dict(procs_map) def run(self, end_time): #1 """排定并顯示事件,直到時間結束""" # 排定各輛計程車的第一個事件 for _, proc in sorted(self.procs.items()): #2 first_event = next(proc) #3 self.events.put(first_event) #4 # 這個仿真系統的主循環 sim_time = 0 #5 while sim_time < end_time: #6 if self.events.empty(): #7 print('*** end of events ***') break current_event = self.events.get() #8 sim_time, proc_id, previous_action = current_event #9 print('taxi:', proc_id, proc_id * ' ', current_event) #10 active_proc = self.procs[proc_id] #11 # 傳入前一個動作,把結果加到sim_time上,獲得下一次活動的時刻 next_time = sim_time + compute_duration(previous_action) #12 try: next_event = active_proc.send(next_time) #13 except StopIteration: del self.procs[proc_id] #14 else: self.events.put(next_event) #15 else: #16 msg = '*** end of simulation time: {} events pending ***' print(msg.format(self.events.qsize()))
- 本章小結:
- 生成器有三種不同代碼編寫風格:
- 傳統的拉取式,疊代器
- 推送式,計算平均值
- 任務式,協程
- 假如沒有協程,我們要寫一個并發程式。可能有以下問題:
- 使用最正常的同步程式設計要實作異步并發效果并不理想,或者難度極高
- 由于GIL鎖的存在,多線程的運作需要頻繁的加鎖解鎖,切換線程,這極大地降低了并發性能
- 而協程的出現,剛好可以解決以上的問題。它的特點有:
- 協程是在單線程裡實作任務的切換的
- 利用同步的方式去實作異步
- 不再需要鎖,提高了并發性能
- 深入了解yield from
- 事件驅動型架構(如 Tornado 和 asyncio)的運作方式:
- 在單個線程中使用一個主循環驅動協程執行并發活動
- 使用協程做面向事件程式設計時,協程會不斷把控制權讓步給主循環,激活并向前運作其他協程,進而執行各個并發活動
- 這是一種協作式多任務:協程顯式自主地把控制權讓步給中央排程程式
- 而多線程實作的是搶占式多任務。排程程式可以在任何時刻暫停線程(即使在執行一個語句的過程中),把控制權讓給其他線程。
- 寬泛的、不正式的對協程的定義:通過客戶調用 .send(…) 方法發送資料或使用 yield from 結構驅動的生成器函數
- asyncio 庫建構在協程之上,不過采用的協程定義更為嚴格
- 在 asyncio 庫中,協程(通常)使用 @asyncio.coroutine 裝飾器裝飾
- 而且始終使用 yield from 結構驅動
- 而不通過直接在協程上調用 .send(…) 方法驅動
- 當然,在 asyncio 庫的底層,協程使用 next(…) 函數和 .send(…) 方法驅動,不過在使用者代碼中隻使用 yield from 結構驅動協程運作
- SimPy 是使用标準的 Python 開發的基于程序的離散事件仿真架構,事件排程程式基于 Python 的生成器實作,是以還可用于異步網絡或實作多智能體系統(即可模拟,也可真正通信)
- Python 3.5 已經接受了 PEP 492,增加了兩個關鍵字:async 和 await
- Python Async/Await入門指南
- 在3.5過後,我們可以使用async修飾将普通函數和生成器函數包裝成異步函數和異步生成器
- 生成器有三種不同代碼編寫風格:
17. 使用期物處理并發
- 期物:future,期物指一種對象,表示異步執行的操作
- 這個概念的作用很大,是 concurrent.futures 子產品和 asyncio 包的基礎
- 網絡下載下傳的三種風格
- 對并發下載下傳的腳本來說,每次下載下傳的順序都不同
- 拒絕服務:Denial-of-Service,DoS
- 在 I/O 密集型應用中,如果代碼寫得正确,那麼不管使用哪種并發政策(使用線程或 asyncio 包),吞吐量都比依序執行的代碼高很多
- 第一種,直接按順序下載下傳棋子
import os import time import sys import requests POP20_CC = ('CN IN US ID BR P1K NG BD RU JP MX PH VN ET EG DE IR TR CD FR').split() BASE_URL = 'http://flupy.org/data/flags' DEST_DIR = './downloads/' def save_flag(img, filename): path = os.path.join(DEST_DIR, filename) with open(path, 'wb') as fp: fp.write(img) def get_flag(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = requests.get(url) return resp.content def show(text): print(text, end=' ') sys.stdout.flush() def download_many(cc_list): for cc in sorted(cc_list): image = get_flag(cc) show(cc) save_flag(image, cc.lower() + '.gif') return len(cc_list) def main(download_many): t0 = time.time() count = download_many(POP20_CC) elapsed = time.time() - t0 msg = '\n{} flags downloaded in {:.2f}s' print(msg.format(count, elapsed)) if __name__ == '__main__': main(download_many) # BD BR CD CN DE EG ET FR ID IN IR JP MX NG P1K PH RU TR US VN # 20 flags downloaded in 21.82s
- 第二種,使用
子產品下載下傳concurrent.futures
from concurrent import futures from flags import save_flag, get_flag, show, main MAX_WORKERS = 20 def download_one(cc): image = get_flag(cc) show(cc) save_flag(image, cc.lower() + '.gif') return cc def download_many(cc_list): # 設定工作的線程數:允許的最大值與要處理的數量之間的 最小值,以免建立多餘的線程 workers = min(MAX_WORKERS, len(cc_list)) with futures.ThreadPoolExecutor(workers) as executor: res = executor.map(download_one, sorted(cc_list)) return len(list(res)) if __name__ == '__main__': main(download_many) # BD CD BR TR IR FR JP IN MX VNET PH NG EG CN ID DE P1K US RU # 20 flags downloaded in 1.05s
- concurrent.futures 子產品的主要特色是 ThreadPoolExecutor 和 ProcessPoolExecutor 類
- 這兩個類實作的接口能分别在不同的線程或程序中執行可調用的對象
- 這兩個類在内部維護着一個工作線程或程序池,以及要執行的任務隊列
- 從 Python 3.4 起,标準庫中有兩個名為 Future 的類:concurrent.futures.Future 和 asyncio.Future
- 期物封裝待完成的操作,可以放入隊列,完成的狀态可以查詢,得到結果(或抛出異常)後可以擷取結果(或異常)
- 通常情況下自己不應該建立期物,而隻能由并發架構(concurrent.futures 或 asyncio)執行個體化
- 原因很簡單:期物表示終将發生的事情,而确定某件事會發生的唯一方式是執行的時間已經排定
- 是以,隻有排定把某件事交給 concurrent.futures.Executor 子類處理時,才會建立 concurrent.futures.Future 執行個體
- 用戶端代碼不應該改變期物的狀态,并發架構在期物表示的延遲計算結束後會改變期物的狀态,而我們無法控制計算何時結束
-
:這個方法不阻塞,傳回值是布爾值,指明期物連結的可調用對象是否已經執行.done()
-
:這個方法隻有一個參數,類型是可調用的對象,期物運作結束後會調用指定的可調用對象.add_done_callback()
-
:傳回可調用對象的結果,或者重新抛出執行可調用的對象時抛出的異常.result()
- 如果期物沒有運作結束,result 方法在兩個 Future 類中的行為相差很大
- 對concurrency.futures.Future 執行個體來說,調用
方法會阻塞調用方所在的線程,直到有結果可傳回。此時,result 方法可以接收可選的 timeout 參數,如果在指定的時間内期物沒有運作完畢,會抛出 TimeoutError 異常f.result()
- asyncio.Future.result 方法不支援設定逾時時間,在那個庫中擷取期物的結果最好使用 yield from 結構
- 這兩個庫中有幾個函數會傳回期物,其他函數則使用期物,以使用者易于了解的方式實作自身
- Executor.map 方法屬于後者:傳回值是一個疊代器,疊代器的
方法調用各個期物的result 方法,是以我們得到的是各個期物的結果,而非期物本身__next__
-
:這個函數的參數是一個期物清單,傳回值是一個疊代器,在期物運作結束後産出期物concurrent.futures.as_completed
- 把download_many 函數中的 executor.map 方法換成 executor.submit 方法和 futures.as_completed 函數
def download_many(cc_list): cc_list = cc_list[:5] with futures.ThreadPoolExecutor(max_workers=3) as executor: to_do = [] for cc in sorted(cc_list): future = executor.submit(download_one, c) to_do.append(future) msg = 'Scheduled for {}: {}' print(msg.format(cc, future)) results = [] for future in futures.as_completed(to_do): res = future.result() msg = '{} result: {!r}' print(msg.format(future, res)) results.append(res) return len(results)
- GIL:Global Interpreter Lock,全局解釋器鎖
- GIL 幾乎對 I/O 密集型處理無害
- 阻塞型I/O和GIL
- CPython 解釋器本身就不是線程安全的,是以有全局解釋器鎖(GIL),一次隻允許使用一個線程執行 Python 位元組碼
- 是以,一個 Python 程序通常不能同時使用多個 CPU 核心
- 編寫 Python 代碼時無法控制 GIL;不過,執行耗時的任務時,可以使用一個内置的函數或一個使用 C 語言編寫的擴充釋放 GIL
- 标準庫中所有執行阻塞型 I/O 操作的函數,在等待作業系統傳回結果時都會釋放 GIL,這意味着在 Python 語言這個層次上可以使用多線程,而 I/O 密集型 Python 程式能從中受益
- 一個 Python 線程等待網絡響應時,阻塞型 I/O 函數會釋放 GIL,再運作一個線程
- 使用concurrent.futures子產品啟動程序
- 如果需要做 CPU 密集型處理,使用這個子產品的ProcessPoolExecutor類能繞開 GIL,利用所有可用的 CPU 核心
- 對簡單的用途來說,ThreadPoolExecutor和ProcessPoolExecutor這兩個實作Executor接口的類唯一值得注意的差別是,
-
方法需要max_workers參數,制定線程池中線程的數量ThreadPoolExecutor.__init__
- 在 ProcessPoolExecutor 類中,那個參數是可選的,而且大多數情況下不使用——預設值是
函數傳回的 CPU 數量os.cpu_count()
-
- 實驗Executor.map方法
- Executor.map 函數傳回結果的順序與調用開始的順序一緻
- 不過,通常更可取的方式是,不管送出的順序,隻要有結果就擷取。為此,要把 Executor.submit 方法和 futures.as_completed 函數結合起來使用
- executor.submit 和 futures.as_completed 這個組合比executor.map 更靈活,因為 submit 方法能處理不同的可調用對象和參數,而 executor.map 隻能處理參數不同的同一個可調用對象
- 傳給 futures.as_completed 函數的期物集合可以來自多個 Executor 執行個體
- 顯示下載下傳進度并處理錯誤
- flags2系列示例處理錯誤的方式
- 使用
函數futures.as_completed
- 線程和多程序的替代方案
- concurrent.futures 是使用線程的最新方式
- 如果
futures.ThreadPoolExecutor
類對某個作業來說不夠靈活,可能要
使用 threading 子產品中的元件(如 Thread、Lock、Semaphore 等)自行制定方案
- 對 CPU 密集型工作來說,要啟動多個程序,規避 GIL
- 建立多個程序最簡單的方式是,使用 futures.ProcessPoolExecutor 類
- 如果使用場景較複雜,需要更進階的工具。使用 multiprocessing 子產品,API 與 threading 子產品相仿,不過作業交給多個程序處理
- multiprocessing 子產品還能解決協作程序遇到的最大挑戰:在程序之間傳遞資料
- 小結
- 為什麼盡管有 GIL,Python 線程仍然适合 I/O 密集型應用:準庫中每個使用 C 語言編寫的 I/O 函數都會釋放 GIL,是以,當某個線程在等待 I/O 時, Python 排程程式會切換到另一個線程
- 借助
concurrent.futures.ProcessPoolExecutor
類使用多程序,以此繞
開 GIL,使用多個 CPU 核心運作
- 對于 CPU 密集型和資料密集型并行處理,現在有個新工具可用——分布式計算引擎 Apache Spark,Spark 在大資料領域發展勢頭強勁,提供了友好的 Python API,支援把 Python 對象當作資料
-
包:定義了一個@parallel 裝飾器,可以應用到任何函數上,把函數變成非阻塞:調用被裝飾的函數時,函數在一個新程序中執行lelo
-
包提供了一個 parallelize 生成器,能把 for 循環配置設定給多個 CPU 執行python-parallelize
- 這兩個包在内部都使用了 multiprocessing 子產品
- GIL 簡化了 CPython 解釋器和 C 語言擴充的實作,得益于 GIL,Python 有很多 C 語言擴充
- Python 線程特别适合在 I/O 密集型系統中使用
- 在 JavaScript 中,隻能通過回調式異步程式設計實作并發
18. 使用asyncio包處理并發
- 線程與協程對比
- spinner_thread.py
import threading import itertools import time import sys class Signal: go = True def spin(msg, signal): write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) time.sleep(.1) if not signal.go: break write(' ' * len(status) + '\x08' * len(status)) def slow_function(): # 假裝等待I/O一段時間 time.sleep(3) return 42 def supervisor(): signal = Signal() spinner = threading.Thread(target=spin, args=('thinking!', signal)) print('spinner object:', spinner) spinner.start() result = slow_function() signal.go = False spinner.join() return result def main(): result = supervisor() print('Answer:', result) if __name__ == '__main__': main()
- Python 沒有提供終止線程的 API,這是有意為之的。若想關閉線程,必須給線程發送消息,這裡是signal.go屬性
- spinner_asyncio.py:通過協程以動畫形式顯示文本式旋轉指針;使用 @asyncio.coroutine 裝飾器替代線程,實作相同的行為
import asyncio import itertools import sys @asyncio.coroutine #1 def spin(msg): #2 write, flush = sys.stdout.write, sys.stdout, flush for char in itertools.cycle('|/-\\'): status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) try: yield from asyncio.sleep(.1) #3 except asyncio.CancelledError: #4 break write(' ' * len(status) + '\x08' * len(status)) @asyncio.coroutine def slow_function(): #5 # 假裝等待I/O一段時間 yield from asyncio.sleep(3) #6 return 42 @asyncio.coroutine def supervisor(): #7 spinner = asyncio.async(spin('thinking!')) #8 print('spinner object:', spinner) #9 result = yield from slow_function() #10 spinner.cancel() #11 return result def main(): loop = asyncio.get_event_loop() #12 result = loop.run_until_complete(supervisor()) #13 loop.close() print('Answer:', result) if __name__ == '__main__': main()
- 使用
代替yield from asyncio.sleep(.1)
,這樣的休眠不會阻塞事件循環time.sleep(.1)
-
函數排定 spin 協程的運作時間,使用一個Task 對象包裝 spin 協程,并立即傳回asyncio.async(...)
- 使用
- 除非想阻塞主線程,進而當機事件循環或整個應用,否則不要在 asyncio 協程中使用
。如果協程需要在一段時間内什麼也不做,應該使用time.sleep(...)
yield from asyncio.sleep(DELAY)
- 兩種 supervisor 實作之間的主要差別概述如下:
- asyncio.Task 對象差不多與 threading.Thread 對象等效
- Task 對象用于驅動協程,Thread 對象用于調用可調用的對象
- Task 對象不由自己動手執行個體化,而是通過把協程傳給
函數或 loop.create_task(…) 方法擷取asyncio.async(...)
- 擷取的 Task 對象已經排定了運作時間(例如,由
函數排定);Thread 執行個體則必須調用 start 方法,明确告知讓它運作asyncio.async
- 線上程版 supervisor 函數中,slow_function 函數是普通的函數,直接由線程調用。在異步版 supervisor 函數中,slow_function 函數是協程,由 yield from 驅動
- 沒有 API 能從外部終止線程,因為線程随時可能被中斷,導緻系統處于無效狀态。如果想終止任務,可以使用 Task.cancel() 執行個體方法,在協程内部抛出 CancelledError 異常。協程可以在暫停的yield 處捕獲這個異常,處理終止請求
- supervisor 協程必須在 main 函數中由 loop.run_until_complete 方法執行
-
:故意不阻塞asyncio.Future
- asyncio.Future 類與 concurrent.futures.Future 類的接口基本一緻,不過實作方式不同,不可以互換
- asyncio.Future 類的 .result() 方法沒有參數,是以不能指定逾時時間。此外,如果調用 .result() 方法時期物還沒運作完畢,那麼.result() 方法不會阻塞去等待結果,而是抛出asyncio.InvalidStateError 異常
-
使用 yield from 處理期物與使用 add_done_callback 方法處理協程的作用一樣:延遲的操作結束後,事件循環不會觸發回調對象,
而是設定期物的傳回值;而 yield from 表達式則在暫停的協程中生成
傳回值,恢複執行協程
- 因為 asyncio.Future 類的目的是與 yield from 一起使用,是以通常不需要使用以下方法
- 無需調用 my_future.add_done_callback(…),因為可以直接把想在期物運作結束後執行的操作放在協程中 yield from my_future 表達式的後面。這是協程的一大優勢:協程是可以暫停和恢複的函數
- 無需調用 my_future.result(),因為 yield from 從期物中産出的值就是結果(例如,result = yield from my_future)
- 從期物、任務和協程中産出
- 對協程來說,擷取 Task 對象有兩種主要方式:
-
asyncio.async(coro_or_future, *, loop=None)
-
BaseEventLoop.create_task(coro)
-
- 測試腳本中試驗期物和協程:
import asyncio def run_sync(coro_or_future): loop = asyncio.get_event_loop() return loop.run_until_complete(coro_or_future) a = run_sync(some_coroutine())
- 對協程來說,擷取 Task 對象有兩種主要方式:
- spinner_thread.py
- 使用asyncio和aiohttp包下載下傳
- flags_asyncio.py:使用 asyncio 和 aiohttp 包實作的異步下載下傳腳本
import asyncio import aiohttp # 1 from flags import BASE_URL, save_flag, show, main #2 @asyncio.coroutine #3 def get_flag(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = yield from aiohttp.request('GET', url) #4 image = yield from resp.read() #5 return image @asyncio.coroutine def download_one(cc): #6 image = yield from get_flag(cc) #7 show(cc) save_flag(image, cc.lower() + '.gif') return cc def download_many(cc_list): loop = asyncio.get_event_loop() #8 to_do = [download_one(cc) for cc in sorted(cc_list)] #9 wait_coro = asyncio.wait(to_do) #10 res, _ = loop.run_until_complete(wait_coro) #11 loop.close() #12 return len(res) if __name__ == '__main__': main(download_many)
- 雖然函數的名稱是 asyncio.wait,但它不是阻塞型函數。wait 是一個協程,等傳給它的所有協程運作完畢後結束
- wait 函數有兩個關鍵字參數,如果設定了可能會傳回未結束的期物;這兩個參數是timeout 和 return_when
- 【我的了解】 協程,就是給函數加上了
裝飾器的函數,然後裡面需要等待的地方加上了@asyncio.coroutine
語句,如果去掉這些語句,就是正常調用的阻塞型流程。yield from
-
句法能防止阻塞,是因為當協程(即包含yield from代碼的委派生成器)暫停後,控制權回到事件循環手中,再去驅動其他協程;foo期物或協程運作完畢後,把結果傳回給暫停的協程,将其恢複。yield from foo
- 關于 yield from 的用法的兩點陳述:
- 使用 yield from 連結的多個協程最終必須由不是協程的調用方驅動,調用方顯式或隐式(例如,在 for 循環中)在最外層委派生成器上調用 next(…) 函數或 .send(…) 方法
- 鍊條中最内層的子生成器必須是簡單的生成器(隻使用 yield)或 可疊代的對象
- 在 asyncio 包的 API 中使用 yield from 時,上述兩點都成立,不過要注意下述細節:
- 我們編寫的協程鍊條始終通過把最外層委派生成器傳給 asyncio 包 API 中的某個函數(如 loop.run_until_complete(…))驅動。也就是說,使用 asyncio 包時,我們編寫的代碼不通過調用 next(…) 函數或 .send(…) 方法驅動協程——這一點由asyncio 包實作的事件循環去做
-
我們編寫的協程鍊條最終通過 yield from 把職責委托給 asyncio 包中的某個協程函數或協程方法(例如:yield from asyncio.sleep(…)),或者其他庫中實作高層協定的協程(例如:resp = yield from aiohttp.request(‘GET’, url))
也就是說,最内層的子生成器是庫中真正執行 I/O 操作的函數,而
不是我們自己編寫的函數
- 概括起來就是:使用 asyncio 包時,我們編寫的異步代碼中包含由 asyncio 本身驅動的協程(即委派生成器),而生成器最終把職責委托給 asyncio 包或第三方庫(如 aiohttp)中的協程。這種處理方式相當于架起了管道,讓 asyncio 事件循環(通過我們編寫的協程)驅動執行低層異步 I/O 操作的庫函數
- flags_asyncio.py:使用 asyncio 和 aiohttp 包實作的異步下載下傳腳本
- 避免阻塞型調用
- 有兩種方法能避免阻塞型調用中止整個應用程式的程序:
- 在單獨的線程中運作各個阻塞型操作
- 把每個阻塞型操作轉換成非阻塞的異步調用使用
- 為了降低記憶體的消耗,通常使用回調來實作異步調用
- 使用回調時,我們不等待響應,而是注冊一個函數,在發生某件事時調用。這樣,所有調用都是非阻塞的
- 隻有異步應用程式底層的事件循環能依靠基礎設定的中斷、線程、輪詢和背景程序等,確定多個并發請求能取得進展并最終完成,這樣才能使用回調
- 把生成器當作協程使用是異步程式設計的另一種方式。對事件循環來說,調用回調與在暫停的協程上調用 .send() 方法效果差不多。各個暫停的協程是要消耗記憶體,但是比線程消耗的記憶體數量級小。而且,協程能避免可怕的“回調地獄”
- 異步和協程
- 有兩種方法能避免阻塞型調用中止整個應用程式的程序:
- 改進asyncio下載下傳版本
- 使用 asyncio.as_completed 函數
- raise X from Y:連結原來的異常
- Semaphore對象維護着一個内部計數器,若在對象調用 .acquire() 協程方法,計數器則遞減;若在對象上調用 .release() 協程方法,計數器則遞增。
- 如果計數器大于0,那麼調用 .acquire() 方法不會阻塞;可是,如果計數器為0,那麼 .acquire() 方法會阻塞調用這個方法的協程,直到其他協程在同一個Semaphore對象上調用 .release() 方法,讓計數器遞增。
- flags2_asyncio.py
import asyncio import collections import aiohttp from aiohttp import web import tqdm from flags2_common import main, HTTPStatus, Result, save_flag # 預設設為較小的值,防止遠端網站出錯 # 例如503 - Service Temporarily Unavaliable DEFAULT_CONCUR_REQ = 5 MAX_CONCUR_REQ = 1000 class FetchError(Exception): #1 def __init__(self, country_code): self.country_code = country_code @asyncio.coroutine def get_flag(base_url, cc): #2 url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower()) resp = yield from aiohttp.request('GET', url) if resp.status == 200: image = yield from resp.read() return image elif resp.status == 404: raise web.HTTPNotFound() else: raise aiohttp.HttpProcessingError(code=resp.status, message=resp.reason, headers=resp.headers) @asyncio.coroutine def download_one(cc, base_url, semaphore, verbose): #3 # semaphore: 信号标 try: with (yield from semaphore): #4 image = yield from get_flag(base_url, cc) #5 except web.HTTPNotFound: #6 status = HTTPStatus.not_found msg = 'not found' except Exception as exc: raise FetchError(cc) from exc #7 else: save_flag(image, cc.lower() + '.gif') #8 status = HTTPStatus.ok msg = 'OK' if verbose and msg: print(cc, msg) return Result(status, cc) @asyncio.coroutine def downloader_coro(cc_list, base_url, verbose, concur_req): #1 counter = collections.Counter() semaphore = asyncio.Semaphore(concur_req) #2 to_do = [download_one(cc, base_url, semaphore, verbose) for cc in sorted(cc_list)] #3 to_do_iter = asyncio.as_completed(to_do) #4 if not verbose: to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list)) #5 for future in to_do_iter: #6 try: res = yield from future #7 except FetchError as exc: #8 country_code = exc.country_code #9 try: error_msg = exc.__cause__.args[0] #10 except IndexError: error_msg = exc.__cause__.__class__.__name__ #11 if verbose and error_msg: msg = '*** Error for {}: {}' print(msg.format(country_code, error_msg)) status = HTTPStatus.error else: status = res.status counter[status] += 1 #12 return counter #13 def download_many(cc_list, base_url, verbose, concur_req): loop = asyncio.get_event_loop() coro = downloader_coro(cc_list, base_url, verbose, concur_req) counts = loop.run_until_complete(coro) #14 loop.close() #15 return counts if __name__ == '__main__': main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
- 擷取 asyncio.Future 對象的結果,最簡單的方法是使用 yield from,而不是調用 future.result() 方法
- 因為失敗時不能以期物為鍵從字典中擷取國家代碼,是以實作了自定義的 FetchError 異常,包裝網絡異常,并關聯相應的國家代碼,是以在詳細模式中報告錯誤時能顯示國家代碼。
- 使用Executor對象,防止阻塞事件循環
- asyncio 的事件循環在背後維護着一個 ThreadPoolExecutor 對象,我們可以調用 run_in_executor 方法,把可調用的對象發給它執行。
- 在 download_one函數中,save_flag函數會阻塞客戶代碼與 asyncio 事件循環公用的唯一線程,是以儲存檔案時,整個應用程式都會當機。
- 解決方案:
- 之前的 download_one 函數中
@asyncio.coroutine def download_one(cc, base_url, semaphore, verbose): try: ... else: save_flag(image, cc.lower() + '.gif') status = HTTPStatus.ok msg = 'OK' if verbose and msg: ...
- 修改之後的代碼
@asyncio.coroutine def download_one(cc, base_url, semaphore, verbose): try: ... else: loop = asyncio.get_event_loop() loop.run_in_executor(None, save_flag, image, cc.lower() + '.gif') status = HTTPStatus.ok msg = 'OK' if verbose and msg: ...
- run_in_executor 方法的第一個參數是 Executor 執行個體,如果設為None,使用事件循環的預設 ThreadPoolExecutor 執行個體。
- 之前的 download_one 函數中
- 從回調到期物和協程
- 使用協程和yield from結構做異步程式設計,無需使用回調
@asyncio.coroutine def three_stages(request1): reponse1 = yield from api_call1(request1) request2 = step1(response1) response2 = yield from api_call2(request2) request3 = step2(response2) response3 = yield from api_call3(request3) step3(response3) # 必須顯示排程執行 loop.create_task(three_stages(request1))
- 每次下載下傳發起多次請求
@asyncio.coroutine def http_get(url): res = yield from aiohttp.request('GET', url) if res.status == 200: ctype = res.headers.get('Content-type', '').lower() if 'json' in ctype or url.endswith('json'): data = yield from res.json() #1 else: data = yield from res.read() #2 return data elif res.status == 404: raise web.HTTPNotFound() else: raise aiohttp.errors.HttpProcessingError(code=res.status, message=res.reason, headers=res.headers) @asyncio.coroutine def get_country(base_url, cc): url = '{}/{cc}/metadata.json'.format(base_url, cc=cc.lower()) metadata = yield from http_get(url) #3 return metadata['country'] @asyncio.coroutine def get_flag(base_url, cc): url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower()) return (yield from http_get(url)) #4 @asyncio.coroutine def download_one(cc, base_url, semaphore, verbose): try: with (yield from semaphore): #5 image = yield from get_flag(base_url, cc) with (yield from semaphore): country = yield from get_country(base_url, cc) except web.HTTPNotFound: status = HTTPStatus.not_found msg = 'not found' except Exception as exc: raise FetchError(cc) from exc else: country = country.replace(' ', '_') filename = '{}-{}.gif'.format(country, cc) loop = asyncio.get_event_loop() loop.run_in_executor(None, save_flag, image, filename) status = HTTPStatus.ok msg = 'ok' if verbose and msg: print(cc, msg) return Result(status, cc)
- 使用協程和yield from結構做異步程式設計,無需使用回調
- 使用asyncio包編寫伺服器
- 使用asyncio包編寫TCP伺服器
import sys import asyncio from charfinder import UnicodeNameIndex #1 CRLF = b'\r\n' PROMPT = b'?>' index = UnicodeNameIndex() #2 @asyncio.coroutine def handle_queries(reader, writer): #3 while True: #4 writer.write(PROMPT) # 不能使用yield from #5 yield from writer.drain() # 必須使用 yield from #6 data = yield from reader.readline() #7 try: query = data.decode().strip() except UnicodeDecodeError: #8 query = '\x00' client = writer.get_extra_info('peername') #9 print('Received from {}: {!r}'.format(client, query)) #10 if query: if ord(query[:1]) < 32: #11 break lines = list(index.find_description_strs(query)) #12 if lines: writer.writelines(line.encode() + CRLF for line in lines) #13 writer.write(index.status(query, len(lines)).encode() + CRLF) #14 yield from writer.drain() #15 print('Sent {} results'.format(len(lines))) #16 print('Close the client socket') #17 writer.close() #18 def main(address='127.0.0.1', port=2323): #1 port = int(port) loop = asyncio.get_event_loop() server_coro = asyncio.start_server(handle_queries, address, port, loop=loop) #2 server = loop.run_until_complete(server_coro) #3 host = server.sockets[0].getsockname() #4 print('Serving on {}. Hit CTRL-C to stop.'.format(host)) #5 try: loop.run_forever() #6 except KeyboardInterrupt: # 按CTRL-C鍵 pass print('Server shutting down.') server.close() #7 loop.run_until_complete(server.wait_closed()) #8 loop.close() #9 if __name__ == '__main__': main(*sys.argv[1:]) #10
- 使用aiohttp包編寫Web伺服器
- asyncio.start_server 函數和 loop.create_server 方法都是協程,傳回的結果都是 asyncio.Server 對象
- 隻有驅動協程,協程才能做事。而驅動 asyncio.coroutine 裝飾的協程有兩種方法:
- 要麼使用 yield from
- 要麼傳給 asyncio 包中某個參數為協程或期物的函數,例如 run_until_complete
- 更好地支援并發的智能用戶端
- http_charfinder.py
from aiohttp import web import asyncio, sys def home(request): #1 query = request.GET.get('query', '').strip() # 2 print('Query: {!r}'.format(query)) # 3 if query: # 4 descriptions = list(index.find_descriptions(query)) res = '\n'.join(ROW_TPL.format(**vars(descr)) for descr in descriptions) msg = index.status(query, len(descriptions)) else: descriptions = [] res = '' msg = 'Enter words describing characters.' html = template.format(query=query, result=res, message=msg) # 5 print('Sending {} results'.format(len(descriptions))) # 6 return web.Response(content_type=CONTENT_TYPE, text=html) # 7 @asyncio.coroutine def init(loop, address, port): # 1 app = web.Application(loop=loop) #2 app.router.add_route('GET', '/', home) #3 handler = app.make_handler() # 4 server = yield from loop.create_server(handler, address, port) # 5 return server.sockets[0].getsockname() # 6 def main(address='127.0.0.1', port=8889): port = int(port) loop = asyncio.get_event_loop() host = loop.run_until_complete(init(loop, address, port)) # 7 print('Serving on {}. Hit CTRL-C to stop.'.format(host)) try: loop.run_forever() # 8 except KeyboardInterrupt: # 按CTRL-C鍵 pass print('Server shutting down.') loop.close() # 9 if __name__ == '__main__': main(*sys.argv[1:])
- 使用asyncio包編寫TCP伺服器
- 小結:
- 盡管有些函數必然會阻塞,但是為了讓程式持續運作,有兩種解決方案可用:
- 使用多個線程
- 異步調用——以回調或協程的形式實作
- 異步庫依賴于低層線程(直至核心級線程),但是這些庫的使用者無需建立線程,也無需知道用到了基礎設施中的低層線程
- 使用 loop.run_in_executor 方法把阻塞的作業(例如儲存檔案)委托給線程池做
- 使用協程解決回調的主要問題:執行分成多步的異步任務時丢失上下文,以及缺少處理錯誤所需的上下文
- 智能的HTTP用戶端,例如單頁Web應用或智能手機應用,需要快速、輕量級的響應和推送更新。鑒于這樣的需求,伺服器端最好使用異步架構,不要使用傳統的Web架構(如Django)。傳統架構的目的是渲染完整的HTML網頁,而且不支援異步通路資料庫。
- Python 和 Node. js 都有一個問題,而 Go 和 Erlang 從一開始就解決了這個問題:我們編寫的代碼無法輕松地利用所有可用的 CPU 核心。
- 盡管有些函數必然會阻塞,但是為了讓程式持續運作,有兩種解決方案可用:
19. 動态屬性和特性
- 引子
- property:特性,在不改變類接口的前提下,使用存取方法(即讀值方法和設值方法)修改資料屬性
- attribute:屬性,在Python中,資料的屬性和處理資料的方法統稱屬性。
- 使用點号通路屬性時(如 obj.attr),Python 解釋器會調用特殊的方法(如
和__getattr__
)計算屬性__setattr__
- 使用者自己定義的類可以通過
方法實作“虛拟屬性”,當通路不存在的屬性時(如 obj.no_such_attribute),即時計算屬性的值__getattr__
- 使用動态屬性轉換資料
- 使用動态屬性通路JSON類資料
from collections import abc class FrozenJson: """一個隻讀接口,使用屬性表示法通路JSON類對象""" def __init__(self, mapping): self.__data = dict(mapping) #1 def __getattr__(self, name): #2 if hasattr(self.__data, name): return getattr(self.__data, name) #3 else: return FrozenJson.build(self.__data[name]) #4 @classmethod def build(cls, obj): #5 if isinstance(obj, abc.Mapping): #6 return cls(obj) elif isinstance(obj, abc.MutableSequence): #7 return [cls.build(item) for item in obj] else: return obj
- 從随機源中生成或仿效動态屬性名的腳本都必須處理一個問題:原始資料中的鍵可能不适合作為屬性名
- 處理無效屬性名
from collections import abc import keyword class FrozenJson: """一個隻讀接口,使用屬性表示法通路JSON類對象""" def __init__(self, mapping): self.__data = {} for key, value in mapping.items(): if keyword.iskeyword(key): key += '_' self.__data[key] = value def __getattr__(self, name): #2 if hasattr(self.__data, name): return getattr(self.__data, name) #3 else: return FrozenJson.build(self.__data[name]) #4 @classmethod def build(cls, obj): #5 if isinstance(obj, abc.Mapping): #6 return cls(obj) elif isinstance(obj, abc.MutableSequence): #7 return [cls.build(item) for item in obj] else: return obj
- 使用
__new__
方法以靈活的方式建立對象
使用
方法取代__new__
方法,建構可能是也可能不是 FrozenJSON 執行個體的新對象build
from collections import abc class FrozenJSON: """一個隻讀接口,使用屬性表示法通路JSON類對象""" def __new__(cls, arg): #1 if isinstance(arg, abc.Mapping): return super().__new__(cls) #2 elif isinstance(arg, abc.MutableSequence): #3 return [cls(item) for item in arg] else: return arg def __init__(self, mapping): self.__data = {} for key, value in mapping.items(): if keyword.iskeyword(key): key += '_' self.__data[key] = value def __getattr__(self, name): if hasattr(self.__data, name): return getattr(self.__data, name) else: return FrozenJson(self.__data[name]) #4
- 使用shelve子產品調整OSCON資料源的結構
- shelve.open 高階函數傳回一個 shelve.Shelf 執行個體,這是簡單的鍵值對象資料庫,背後由 dbm 子產品支援,具有下述特點:
- shelve.Shelf 是 abc.MutableMapping 的子類,是以提供了處理映射類型的重要方法
- 此外,shelve.Shelf 類還提供了幾個管理 I/O 的方法,如 sync 和 close;它也是一個上下文管理器
- 隻要把新值賦予鍵,就會儲存鍵和值
- 鍵必須是字元串
- 值必須是 pickle 子產品能處理的對象
- schedule1.py:通路儲存在 shelve.Shelf 對象裡的 OSCON 日程資料
import warnings DB_NAME = 'data/schedule1_db' CONFERENCE = 'conference.115' class Record: def __init__(self, **kwargs): self.__dict__.update(kwargs) #2 def load_db(db): raw_data = load() #3 warnings.warn('loading ' + DB_NAME) for collection, rec_list in raw_data['Schedule'].items(): #4 record_type = collection[:-1] #5 for record in rec_list: key = '{}.{}'.format(record_type, record['serial']) #6 record['serial'] = key #7 db[key] = Record(**record) #8
-
方法展示了一個流行的 Python 技巧。我們知道,對象的Record.__init__
屬性中存儲着對象的屬性——前提是類中沒有聲明__dict__
屬性__slots__
- 是以,更新執行個體的
屬性,把值設為一個映射,能快速地在那個執行個體中建立一堆屬性__dict__
-
- shelve.open 高階函數傳回一個 shelve.Shelf 執行個體,這是簡單的鍵值對象資料庫,背後由 dbm 子產品支援,具有下述特點:
- 使用特性擷取連結的記錄:schedule2.py
import warnings import inspect #1 import osconfeed DB_NAME = 'data/schedule2_db' #2 CONFERENCE = 'conference.115' class Record: def __init__(self, **kwargs): self.__dict__.update(kwargs) def __eq__(self, other): #3 if isinstance(other, Record): return self.__dict__ == other.__dict__ else: return NotImplemented class MissingDatabaseError(RuntimeError): """需要資料哭但沒有制定資料庫時抛出""" #1 class DbRecord(Record): #2 __db = None #3 @staticmethod #4 def set_db(db): DbRecord.__db = db #5 @staticmethod #6 def get_db(): return DbRecordc.__db @classmethod #7 def fetch(cls, ident): db = cls.get_db() try: return db[ident] #8 except TypeError: if db is None: #9 msg = "database not set; call '{}.set_db(my_db)'" raise MissingDatabaseError(msg.format(cls.__name__)) else: #10 raise def __repr__(self): if hasattr(self, 'serial'): #11 cls_name = self.__class__.__name__ return '<{} serial={!r}>'.format(cls_name, self.serial) else: return super().__repr__() #12 class Event(DbRecord): #1 @property def venue(self): key = 'venue.{}'.format(self.venue_serial) return self.__class__.fetch(key) #2 @property def speakers(self): if not hasattr(self, '_speaker_objs'): #3 spkr_serials = self.__dict__['speakers'] #4 fetch = self.__class__.fetch #5 self._speaker_objs = [fetch('speaker.{}'.format(key)) for key in spkr_serials] #6 return self._speaker_objs #7 def __repr__(self): if hasattr(self, 'name'): #8 cls_name = self.__class__.__name__ return '<{} {!r}>'.format(cls_name, self.name) else: return super().__repr__() #9 def load_db(db): raw_data = osconfeed.load() warnings.warn('loading ' + DB_NAME) for collection, rec_list in raw_data['Schedule'].items(): record_type = collection[:-1] #1 cls_name = record_type.capitalize() #2 cls = globals().get(cls_name, DbRecord) #3 if inspect.isclass(cls) and issubclass(cls, DbRecord): #4 factory = cls #5 else: factory = DbRecord #6 for record in rec_list: #7 key = '{}.{}'.format(record_type, record['serial']) record['serial'] = key db[key] = factory(**record) #8
- 使用動态屬性通路JSON類資料
- 使用特性驗證屬性
- LineItem類第1版:表示訂單中商品的類
class LineItem: def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- LineItem類第2版:能驗證值的特性
class LineItem: def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price @property def weight(self): return self.__weight @weight.setter def weight(self, value): if value > 0: self.__weight = value else: raise ValueError('value must be > 0')
- LineItem類第1版:表示訂單中商品的類
- 特性全解析
- 特性會覆寫執行個體屬性
- 如果執行個體和所屬的類有同名資料屬性,那麼執行個體屬性會覆寫(或稱遮蓋)類屬性——至少通過那個執行個體讀取屬性時是這樣
- 屬性:類變量(類屬性)、成員變量(執行個體屬性)(我的了解)
- 特性:用@property修飾的,特性是類屬性(我的了解)
- 同名變量會導緻,成員變量覆寫類變量,特性覆寫屬性
- 删除特性後,類屬性和執行個體屬性,都會恢複
- bj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從
開始,而且,僅當類中沒有名為 attr 特性時,Python 才會在 obj 執行個體中尋找obj.__class__
- 特性 其實是 覆寫型描述符
- 特性的文檔
- 控制台中的 help() 函數或 IDE 等工具需要顯示特性的文檔時,會從特性的
屬性中提取資訊__doc__
-
weight = property(get_weight, set_weight, doc='weight in kilograms')
class Foo: @property def bar(self): '''The bar attribute''' return self.__dict__['bar'] @bar.setter def bar(self, value): self.__dict__['bar'] = value
- 控制台中的 help() 函數或 IDE 等工具需要顯示特性的文檔時,會從特性的
- 特性會覆寫執行個體屬性
- 定義一個特性工廠函數
- bulkfood_v2prop.py
def quantity(storage_name): #1 def qty_getter(instance): #2 return instance.__dict__[storage_name] #3 def qty_setter(instance, value): #4 if value > 0: instance.__dict__[storage_name] = value #5 else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter) #6 class LineItem: weight = quantity('weight') price = quantity('price') def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- 對 self.weight 或 nutmeg.weight 的每個引用都由特性函數處理
- 隻有直接存取
屬性才能跳過特性的處理邏輯__dict__
- bulkfood_v2prop.py
- 處理屬性删除操作
- 使用 Python 程式設計時不常删除屬性,通過特性删除屬性更少見
- 對象的屬性可以使用 del 語句删除
class BlackKnight: def __init__(self): self.members = ['an arm', 'another arm', 'a leg', 'another leg'] self.phrases = ["'Tis but a scratch.'", "It's just a flesh wound.", "I'm invinvible!", "All right, we'll call it a draw."] @property def member(self): print('next member is:') return self.members[0] @member.deleter def member(self): text = 'BLACK KNIGHT (loses {})\n-- {}' print(text.format(self.members.pop(0), self.phrases.pop(0)))
- 處理屬性的重要屬性和函數
- 影響屬性處理方式的特殊屬性
-
:對象所屬類的引用(即__class__
與obj.__class__
的作用相同)。Python 的某些特殊方法,例如type(obj)
,隻在對象的類中尋找,而不在執行個體中尋找。__getattr__
-
:一個映射,存儲對象或類的可寫屬性。有__dict__
屬性的對象,任何時候都能随意設定新屬性。如果類有__dict__
屬性,它的執行個體可能沒有__slots__
屬性。__dict__
-
:類可以定義這個這屬性,限制執行個體能有哪些屬性。__slots__
屬性的值是一個字元串組成的元組,指明允許有的屬性。如果__slots__
中沒有__slots__
,那麼該類的執行個體沒有'__dict__'
屬性,執行個體隻允許有指定名稱的屬性。__dict__
屬性的值雖然可以是一個清單,但是最好始終使用元組,因為處理完類的定義體之後再修改__slots__
清單沒有任何作用,是以使用可變的序列容易讓人誤解__slots__
-
- 處理屬性的内置函數
-
:列出對象的大多數屬性dir([object])
- dir 函數能審查有或沒有
屬性的對象__dict__
- dir 函數不會列出
屬性本身,但會列出其中的鍵__dict__
- dir 函數也不會列出類的幾個特殊屬性,例如
、__mro__
和__bases__
__name__
- dir 函數能審查有或沒有
-
:從 object 對象中擷取 name 字元串對應的屬性getattr(object, name[, default])
- 擷取的屬性可能來自對象所屬的類或超類
- 如果沒有指定的屬性,getattr 函數抛出 AttributeError 異常,或者傳回 default 參數的值
-
:如果 object 對象中存在指定的屬性,或者能以某種方式(例如繼承)通過 object 對象擷取指定的屬性,傳回 Truehasattr(object, name)
-
:把 object 對象指定屬性的值設為 value,前提是 object 對象能接受那個值setattr(object, name, value)
- 這個函數可能會建立一個新屬性,或者覆寫現有的屬性
-
:傳回 object 對象的vars([object])
屬性__dict__
- 如果執行個體所屬的類定義了
屬性,執行個體沒有__slots__
屬性,那麼 vars 函數不能處理那個執行個體__dict__
- 如果沒有指定參數,那麼 vars() 函數的作用與 locals() 函數一樣:傳回表示本地作用域的字典
- 如果執行個體所屬的類定義了
-
- 處理屬性的特殊方法
- 使用點号或内置的 getattr、hasattr 和 setattr 函數存取屬性都會觸發下述清單中相應的特殊方法
- 但是,直接通過執行個體的
屬性讀寫屬性不會觸發這些特殊方法__dict__
- 如果需要,通常會使用這種方式跳過特殊方法
- 要假定特殊方法從類上擷取,即便操作目标是執行個體也是如此。是以,特殊方法不會被同名執行個體屬性遮蓋
-
:隻要使用 del 語句删除屬性,就會調用這個方法__delattr__(self, name)
-
:把對象傳給 dir 函數時調用,列出屬性__dir__(self)
-
:僅當擷取指定的屬性失敗,搜尋過 obj、Class 和超類之後調用__getattr__(self, name)
-
__getattribute__(self, name)
:嘗試擷取指定的屬性時總會調用這個方法,不過,尋找的屬性是特殊屬性或特殊方法時除外
為了在擷取 obj 執行個體的屬性時不導緻無限遞歸,
方法的實作要使用__getattribute__
super().__getattribute__(obj, name)
-
:嘗試設定指定的屬性時總會調用這個方法,點号和 setattr 内置函數會觸發這個方法__setattr__(self, name, value)
- 影響屬性處理方式的特殊屬性
- 總結:
- 詳解Python中的
和__init__
__new__
- 在 Python 中,很多情況下類和函數可以互換。這不僅是因為 Python 沒有 new 運算符,還因為有特殊的
方法,可以把類變成工廠方法,生成不同類型的對象,或者傳回事先建構好的執行個體,而不是每次都建立一個新執行個體__new__
- UAP:統一通路原則,Unifrom Access Principle
- new 方法接受的參數雖然也是和 init 一樣,但 init 是在類執行個體建立之後調用,而 new 方法正是建立這個類執行個體的方法
- 詳解Python中的
20. 屬性描述符
- 前言
- 描述符是對多個屬性運用相同存取邏輯的一種方式
- 描述符是實作了特定協定的類,這個協定包括
、__get__
和__set__
方法__delete__
- 除了特性之外,使用描述符的 Python 功能還有方法及 classmethod 和 staticmethod 裝飾器
- 描述符示例:驗證屬性
- LineItem類第3版:一個簡單的描述符
- 描述符的用法是,建立一個執行個體,作為另一個類的類屬性
- 描述符類:實作描述符協定的類
- 托管類:把描述符執行個體聲明為類屬性的類
- 描述符執行個體:描述符類的各個執行個體,聲明為托管類的類屬性
- 托管執行個體:托管類的執行個體
- 儲存屬性:托管執行個體中存儲自身托管屬性的屬性
- 托管屬性:托管類中由描述符執行個體處理的公開屬性,值存儲在儲存屬性中。也就是說,描述符執行個體和儲存屬性為托管屬性建立了基礎
- 代碼
class Quantity: #1 def __init__(self, storage_name): self.storage_name = storage_name #2 def __set__(self, instance, value): #3 if value > 0: instance.__dict__[self.storage_name] = value #4 else: raise ValueError('value must be > 0') class LineItem: weight = Quantity('weight') #5 price = Quantity('price') #6 def __init__(self, description, weight, price): #7 self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- LineItem類第4版:自動擷取儲存屬性的名稱
- 這裡可以使用内置的高階函數 getattr 和 setattr 存取值,無需使用
,因為托管屬性和儲存屬性的名稱不同,是以把儲存屬性傳給 getattr 函數不會觸發描述符,不會像前面那樣出現無限遞歸instance.__dict__
class Quantity: __counter = 0 #1 def __init__(self): cls = self.__class__ #2 prefix = cls.__name__ index = cls.__counter self.storage_name = '_{}#{}'.format(prefix, index) #3 cls.__counter += 1 #4 def __get__(self, instance, owner): #5 return getattr(instance, self.storage_name) #6 def __set__(self, instance, value): #6 if value > 0: setattr(instance, self.storage_name, value) #7 else: raise ValueError('value must be > 0') class LineItem: weight = Quantity() #9 price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- 為了給使用者提供内省和其他元程式設計技術支援,通過類通路托管屬性時,最好讓
方法傳回描述符執行個體__get__
def __get__(self, instance, owner): #5 if instance is None: return self else: return getattr(instance, self.storage_name) #6
- 描述符的正常用法:整潔的 LineItem 類;Quantity 描述符類現在位于導入的 model_v4c 子產品中
import model_v4c as model class LineItem: weight = model.Quantity() price = model.Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- Django 模型的字段就是描述符
- 使用特性工廠函數實作與上述示例中的描述符類相同的功能
def quantity(): #1 try: quantity.counter += 1 #2 except AttributeError: quantity.counter = 0 #3 storage_name = '_{}:{}'.format('quntity', quantity.counter) #4 def qty_getter(instance): #5 return getattr(instance, storage_name) def qty_setter(instance, value): if value > 0: setattr(instance, storage_name, value) else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter)
- 不能依靠類屬性在多次調用之間共享 counter,是以把它定義為 quantity 函數自身的屬性
- 作者更喜歡描述符類的方式,因為:
- 描述符類可以使用子類擴充;若想重用工廠函數中的代碼,除了複制粘貼,很難有其他方法
- 與使用函數屬性和閉包保持狀态相比,在類屬性和執行個體屬性中保持狀态更易于了解
- 從某種程度上來講,特性工廠函數模式較簡單,可是描述符類方式更易擴充,而且應用也更廣泛
- 這裡可以使用内置的高階函數 getattr 和 setattr 存取值,無需使用
- LineItem類第5版:一種新型描述符
import abc class AutoStorage: #1 __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = '_{}#{}'.format(prefix, index) cls.__counter += 1 def __get__(self, instance, owner): if instance is None: return self else: return getattr(instance, self.storage_name) def __set__(self, instance, value): setattr(instance, self.storage_name, value) #2 class Validated(abc.ABC, AutoStorage): #3 def __set__(self, instance, value): value = self.validate(instance, value) #4 super().__set__(instance, value) #5 @abc.abstractmethod def validate(self, instance, value): #6 """return validated value or raise ValueError""" class Quantity(Validated): #7 """a number greater than zero""" def validate(self, instance, value): if value <= 0: raise ValueError('value must be > 0') return value class NonBlank(Validated): """a string with at least one ono-space character""" def validate(self, instance, value): value = value.strip() if len(value) == 0: raise ValueError('value cannot be empty or blank') return value #8 class LineItem: description = NonBlank() weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
- 這種描述符也叫覆寫型描述符,因為描述符的
方法使用托管執行個體中的同名屬性覆寫(即插手接管)了要設定的屬性__set__
- 上述代碼的類圖
- 這種描述符也叫覆寫型描述符,因為描述符的
- LineItem類第3版:一個簡單的描述符
- 覆寫型與非覆寫型描述符對比
- Python 存取屬性的方式特别不對等
- 通過執行個體讀取屬性時,通常傳回的是執行個體中定義的屬性
- 如果執行個體中沒有指定的屬性,那麼會擷取類屬性
- 而為執行個體中的屬性指派時,通常會在執行個體中建立屬性,根本不影響類
- 覆寫型描述符
- 實作
方法的話,會覆寫對執行個體屬性的指派操作__set__
- 特性也是覆寫型描述符:如果沒提供設值函數,property 類中的
方法會抛出 AttributeError 異常,指明那個屬性是隻讀的__set__
- 代碼樣例
def cls_name(obj_or_cls): cls = type(obj_or_cls) if cls is type: cls = obj_or_cls return cls.__name__.split('.')[-1] def display(obj): cls = type(obj) if cls is type: return '<class {}>'.format(obj.__name__) elif cls in [type(None), int]: return repr(obj) else: return '<{} object>'.format(cls_name(obj)) def print_args(name, *args): pseudo_args = ', '.join(display(x) for x in args) print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args)) class Overriding: #1 """也稱資料描述符或強制描述符""" def __get__(self, instance, owner): print_args('get', self, instance, owner) #2 def __set__(self, instance, value): print_args('set', self, instance, value) class OverridingNoGet: #3 """沒有``__get__``方法的覆寫性描述符""" def __set__(self, instance, value): print_args('set', self, instance, value) class NonOverriding: #4 """也稱非資料描述符或遮蓋型描述符""" def __get__(self, instance, owner): print_args('get', self, instance, owner) class Managed: #5 over = Overriding() over_no_get = OverridingNoGet() non_over = NonOverriding() def spam(self): #6 print('-> Managed.spam({})'.format(display(self)))
- 實作
- 沒有
方法的覆寫型描述符__get__
- 執行個體屬性會遮蓋描述符,不過隻有讀操作是如此
- 讀取時,隻要有同名的執行個體屬性,描述符就會被遮蓋
- 非覆寫型描述符
- 沒有實作
方法的描述符是非覆寫型描述符__set__
- obj 有個名為 non_over 的執行個體屬性,把 Managed 類的同名描述符屬性遮蓋掉
- 在上述幾個示例中,我們為幾個與描述符同名的執行個體屬性賦了值,結果依描述符中是否有
方法而有所不同__set__
- 依附在類上的描述符無法控制為類屬性指派的操作。其實,這意味着為類屬性指派能覆寫描述符屬性
- 沒有實作
- 在類中覆寫描述符
- 不管描述符是不是覆寫型,為類屬性指派都能覆寫描述符
- 這是一種猴子更新檔技術
- 讀寫屬性的另一種不對等:
- 讀類屬性的操作可以由依附在托管類上定義有
方法的描述符處理__get__
- 但是寫類屬性的操作不會由依附在托管類上定義有
方法的描述符處理__set__
- 若想控制設定類屬性的操作,要把描述符依附在類的類上,即依附在元類上
- 讀類屬性的操作可以由依附在托管類上定義有
- Python 存取屬性的方式特别不對等
- 方法是描述符
- 在類中定義的函數屬于綁定方法(bound method)
- 過托管類通路時,函數的
方法會傳回自身的引用__get__
- 通過執行個體通路時,函數的
方法傳回的是綁定方法對象:一種可調用的對象,裡面包裝着函數,并把托管執行個體(例如 obj)綁定給函數的第一個參數(即 self),這與 functools.partial 函數的行為一緻__get__
- function:函數;method:方法
- 函數會變成綁定方法,這是 Python 語言底層使用描述符的最好例證
- 描述符用法建議
- 使用特性以保持簡單
- 内置的 property 類建立的其實是覆寫型描述符,
方法和__set__
方法都實作了,即便不定義設值方法也是如此__get__
- 特性的
方法預設抛出 AttributeError: can’t set attribute__set__
- 是以建立隻讀屬性最簡單的方式是使用特性,這能避免下一條所述的問題
- 内置的 property 類建立的其實是覆寫型描述符,
- 隻讀描述符必須有
方法__set__
- 如果使用描述符類實作隻讀屬性,要記住,
和__get__
兩個方法必須都定義__set__
- 否則,執行個體的同名屬性會遮蓋描述符
- 隻讀屬性的
方法隻需抛出 AttributeError 異常,并提供合适的錯誤消息__set__
- 如果使用描述符類實作隻讀屬性,要記住,
- 用于驗證的描述符可以隻有
方法__set__
- 對僅用于驗證的描述符來說,
方法應該檢查 value 參數獲得的值,如果有效,使用描述符執行個體的名稱為鍵,直接在執行個體的__set__
屬性中設定__dict__
- 這樣,從執行個體中讀取同名屬性的速度很快,因為不用經過
方法處理__get__
- 對僅用于驗證的描述符來說,
- 僅有
方法的描述符可以實作高效緩存__get__
- 如果隻編寫了
方法,那麼建立的是非覆寫型描述符__get__
- 這種描述符可用于執行某些耗費資源的計算,然後為執行個體設定同名屬性,緩存結果
- 同名執行個體屬性會遮蓋描述符,是以後續通路會直接從執行個體的
屬性中擷取值,而不會再觸發描述符的__dict__
方法__get__
- 如果隻編寫了
- 非特殊的方法可以被執行個體屬性遮蓋
- 由于函數和方法隻實作了
方法,它們不會處理同名執行個體屬性的指派操作__get__
- 特殊方法不受這個問題的影響
- 釋器隻會在類中尋找特殊的方法,也就是說,repr(x) 執行的其實是
,是以 x 的x.__class__.__repr__(x)
__repr__
屬性對 repr(x) 方
法調用沒有影響
- 出于同樣的原因,執行個體的
屬性不會破壞正常的屬性通路規則__getattr__
- 由于函數和方法隻實作了
- 使用特性以保持簡單
- 描述符的文檔字元串和覆寫删除操作
- 在描述符類中,實作正常的
和(或)__get__
方法之外,可以實作__set__
方法,或者隻實作__delete__
方法做到這一點__delete__
- python中函數和方法的差別
-
函數:def定義的,或者内置的,或者lambda
與類和執行個體無綁定關系的function都屬于函數(function)
- 方法:跟類有關的,
、__init__
與類和執行個體有綁定關系的function都屬于方法(method)def(self)
-
- 在描述符類中,實作正常的
21. 類元程式設計
- 前言
- 類元程式設計是指在運作時建立或定制類的技藝
- 元類是類元程式設計最進階的工具:使用元類可以建立具有某種特質的全新類種,例如我們見過的抽象基類
- 除非開發架構,否則不要編寫元類
- 類工廠函數
- record_factory.py:一個簡單的類工廠函數
def record_factory(cls_name, field_names): try: field_names = field_names.replace(',', ' ').split() #1 except AttributeError: # 不能調用.replace或.split方法 pass # 假定field_names本就是辨別符組成的序列 field_names = tuple(field_names) #2 def __init__(self, *args, **kwargs): attrs = dict(zip(self.__slots__, args)) attrs.update(kwargs) for name, value in attrs.items(): setattr(self, name, value) def __iter__(self): #4 for name in self.__slots__: yield getattr(self, name) def __repr__(self): #5 values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__, self)) return '{}({})'.format(self.__class__.__name__, values) cls_attrs = dict( __slots__ = field_names, __init__ = __init__, __iter__ = __iter__, __repr__ = __repr ) #6 return type(cls_name, (object,), cls_attrs) #7
- 通常,我們把 type 視作函數,因為我們像函數那樣使用它。調用 type(my_object) 擷取對象所屬的類,作用與
相同my_object.__class__
- type 當成類使用時,傳入三個參數可以建立一個類
- record_factory 函數建立的類,其執行個體有個局限——不能序列化,即不能使用 pickle 子產品裡的 dump/load 函數處理
- record_factory.py:一個簡單的類工廠函數
- 定制描述符的類裝飾器
- 我們要在建立類時設定儲存屬性的名稱,用類裝飾器或元類可以做到這一點
- 類裝飾器與函數裝飾器非常類似,是參數為類對象的函數,傳回原來的類或修改後的類
- model_v6.py:一個類裝飾器
def entity(cls): #1 for key, attr in cls.__dict__.items(): #2 if isinstance(attr, Validated): #3 type_name = type(attr).__name__ attr.strorage_name = '_{}#{}'.format(type_name, key) #4 return cls #5
- 類裝飾器有個重大缺點:隻對直接依附的類有效;這意味着,被裝飾的類的子類可能繼承也可能不繼承裝飾器所做的改動,具體情況視改動的方式而定
- 導入時和運作時比較
- 在導入時,解釋器會從上到下一次性解析完 .py 子產品的源碼,然後生成用于執行的位元組碼。如果句法有錯誤,就在此時報告。如果本地的
檔案夾中有最新的 .pyc 檔案,解釋器會跳過上述步驟,因為已經有運作所需的位元組碼了__pycache__
- import 語句,它不隻是聲明,在程序中首次導入子產品時,還會運作所導入子產品中的全部頂層代碼——以後導入相同的子產品則使用緩存,隻做名稱綁定
- 那些頂層代碼可以做任何事,包括通常在“運作時”做的事,例如連接配接資料庫
- 是以,“導入時”與“運作時”之間的界線是模糊的:import 語句可以觸發任何“運作時”行為
- 解釋器在導入時定義頂層函數,但是僅當在運作時調用函數時才會執行函數的定義體
- 對類來說,情況就不同了:
- 在導入時,解釋器會執行每個類的定義體,甚至會執行嵌套類的定義體
- 執行類定義體的結果是,定義了類的屬性和方法,并建構了類對象
- 從這個意義上了解,類的定義體屬于“頂層代碼”,因為它在導入時運作
- 場景示範
- evalsupport.py
print('<[100]> evalsupport module start') def deco_alpha(cls): print('<[200]> deco_alpha') def inner_1(self): print('<[300]> deco_alpha:inner_1') cls.method_y = inner_1 return cls class MetaAleph(type): print('<[400]> MetaAleph body') def __init__(cls, name, bases, dic): print('<[500]> MetaAleph.__init__') def inner_2(self): print('<[600]> MetaAleph.__init__:init_2') cls.method_z = inner_2 print('<[700]> evalsupport module end')
- evaltime.py
from evalsupport import deco_alpha print('<[1]> evaltime module start') class ClassOne(): print('<[2]> ClassOne body') def __init__(self): print('<[3]> ClassOne.__init__') def __del__(self): print('<[4]> ClassOne.__del_-') def method_x(self): print('<[5]> ClassOne.method_x') class ClassTwo(object): print('<[6]> ClassTwo body') @deco_alpha class ClassThree(): print('<[7]> ClassThree body') def method_y(self): print('<[8]> ClassThree.method_y') class ClassFour(ClassThree): print('<[9]> ClassFour body') def method_y(self): print('<[10]> ClassFour.method_y') if __name__ == '__main__': print('<[11]> ClassOne tests', 30 * '.') one = ClassOne() one.method_x() print('<[12]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[13]> CLassFour tests', 30 * '.') four = ClassFour() four.method_y() print('<[14]> evaltime module end')
- 結果
>>> import evaltime <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime module start <[2]> ClassOne body <[6]> ClassTwo body <[7]> ClassThree body <[200]> deco_alpha <[9]> ClassFour body <[14]> evaltime module end
- 結論:
- 這個場景由簡單的 import evaltime 語句觸發
- 解釋器會執行所導入子產品及其依賴(evalsupport)中的每個類定義體
- 解釋器先計算類的定義體,然後調用依附在類上的裝飾器函數,這是合理的行為,因為必須先建構類對象,裝飾器才有類對象可處理
- evalsupport.py
- 場景2:
python evaltime.py
- 結果
python evaltime.py <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime module start <[2]> ClassOne body <[6]> ClassTwo body <[7]> ClassThree body <[200]> deco_alpha <[9]> ClassFour body <[11]> ClassOne tests .............................. <[3]> ClassOne.__init__ <[5]> ClassOne.method_x <[12]> ClassThree tests .............................. <[300]> deco_alpha:inner_1 <[13]> CLassFour tests .............................. <[10]> ClassFour.method_y <[14]> evaltime module end <[4]> ClassOne.__del_-
- 結論:
- 類裝飾器可能對子類沒有影響
- 當然,如果
方法使用ClassFour.method_y
調用super(...)
方法,我們便會看到裝飾器起作用,執行 inner_1 函數ClassThree.method_y
- 結果
- 在導入時,解釋器會從上到下一次性解析完 .py 子產品的源碼,然後生成用于執行的位元組碼。如果句法有錯誤,就在此時報告。如果本地的
- 元類基礎知識
- 元類是制造類的工廠,是用于建構類的類
- 根據 Python 對象模型,類是對象,是以類肯定是另外某個類的執行個體
- 預設情況下,Python 中的類是 type 類的執行個體
- type 是大多數内置的類和使用者定義的類的元類
- 左邊的示意圖強調 str、type 和 LineItem 是 object 的子類;右邊的示意圖則清楚地表明 str、object 和 LineItem 是 type 的執行個體
- object 類和 type 類之間的關系很獨特:object 是 type 的執行個體,而 type 是 object 的子類
- 所有類都直接或間接地是 type 的執行個體,不過隻有元類同時也是 type 的子類
- 元類(如 ABCMeta)從 type 類繼承了建構類的能力
- 所有類都是 type 的執行個體,但是元類還是 type 的子類,是以可以作為制造類的工廠
- 元類可以通過實作
方法定制執行個體。元類的__init__
方法可以做到類裝飾器能做的任何事情,但是作用更大__init__
- evaltime_meta.py:ClassFive 是 MetaAleph 元類的執行個體
from evalsupport import deco_alpha from evalsupport import MetaAleph print('<[1]> evaltime_meta module start') @deco_alpha class ClassThree(): print('<[2]> ClassThree body') def method_y(self): print('<[3]> ClassThree.method_y') class ClassFour(ClassThree): print('<[4]> ClassFour body') def method_y(self): print('<[5]> ClassFour.method_y') class ClassFive(metaclass=MetaAleph): print('<[6]> CLassFive body') def __init__(self): print('<[7]> ClassFive.__init__') def method_z(self): print('<[8]> ClassFive.method_z') class ClassSix(ClassFive): print('<[9]> ClassSix body') def method_z(self): print('<[10]> ClassSix.method_z') if __name__ == '__main__': print('<[11]> ClassThree tests', 30 * '.') three = ClassThree() three.method_y() print('<[12]> ClassFour tests', 30 * '.') four = ClassFour() four.method_y() print('<[13]> ClassFive tests', 30 * '.') five = ClassFive() five.method_z() print('<[14]> ClassSix tests', 30 * '.') six = ClassSix() six.method_z() print('<[15]> evaltime_meta module end')
- 場景3:在 Python 控制台中以互動的方式導入 evaltime_meta.py 子產品
>>> import evaltime_meta <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime_meta module start <[2]> ClassThree body <[200]> deco_alpha <[4]> ClassFour body <[6]> CLassFive body <[500]> MetaAleph.__init__ <[9]> ClassSix body <[500]> MetaAleph.__init__ <[15]> evaltime_meta module end
- 場景4:在指令行中運作 evaltime_meta.py 子產品
python evaltime_meta.py <[100]> evalsupport module start <[400]> MetaAleph body <[700]> evalsupport module end <[1]> evaltime_meta module start <[2]> ClassThree body <[200]> deco_alpha <[4]> ClassFour body <[6]> CLassFive body <[500]> MetaAleph.__init__ <[9]> ClassSix body <[500]> MetaAleph.__init__ <[11]> ClassThree tests .............................. <[300]> deco_alpha:inner_1 <[12]> ClassFour tests .............................. <[5]> ClassFour.method_y <[13]> ClassFive tests .............................. <[7]> ClassFive.__init__ <[600]> MetaAleph.__init__:init_2 <[14]> ClassSix tests .............................. <[7]> ClassFive.__init__ <[600]> MetaAleph.__init__:init_2 <[15]> evaltime_meta module end
- 編寫元類時,通常會把 self 參數改成 cls;在元類的
方法中,把第一個參數命名為 cls 能清楚地表明要建構的執行個體是類__init__
- 裝飾器裝飾的類産生的效果不會影響其子類;而通過metaclass設定了原類的類,産生的效果會影響其子類
- 定制描述符的元類
class EntityMeta(type): """元類,用于建立帶有驗證字段的業務實體""" def __init__(cls, name, bases, attr_dict): super().__init__(name, bases, attr_dict) #1 for key, attr in attr_dict.items(): #2 if isinstance(attr, Validated): type_name = type(attr).__name__ attr.storage_name = '_{}#{}'.format(type_name, key) class Entity(metaclass=EntityMeta): #3 """帶有驗證字段的業務實體"""
class LineItem(Entity): ...
- 元類的特殊方法
__prepare__
- 元類或類裝飾器獲得映射時,屬性在類定義體中的順序已經丢失了(因為名稱到屬性的映射是字典)
- 這個問題的解決辦法是,使用 Python 3 引入的特殊方法
__prepare__
- 這個特殊方法隻在元類中有用,而且必須聲明為類方法(即,要使用 @classmethod 裝飾器定義)
- 解釋器調用元類的
方法之前會先調用__new__
方法,使用類定義體中的屬性建立映射__prepare__
-
方法的第一個參數是元類,随後兩個參數分别是要建構的類的名稱和基類組成的元組,傳回值必須是映射__prepare__
- 元類建構新類時,
方法傳回的映射會傳給__prepare__
方法的最後一個參數,然後再傳給__new__
方法__init__
- 代碼
import collections class EntityMeta(type): """元類,用于建立帶有驗證字段的業務實體""" @classmethod def __prepare__(cls, name, bases): return collections.OrderedDict() def __init__(cls, name, bases, attr_dict): super().__init__(name, bases, attr_dict) cls._field_names = [] for key, attr in attr_dict.items(): if isinstance(attr, Validated): type_name = type(attr).__name__ attr.storage_name = '_{}#{}'.format(type_name, key) cls._field_names.append(key) class Entity(metaclass=EntityMeta): #3 """帶有驗證字段的業務實體""" @classmethod def field_names(cls): for name in cls._field_names: yield name
- 在現實世界中,架構和庫會使用元類協助程式員執行很多任務:
- 驗證屬性
- 一次把裝飾器依附到多個方法上
- 序列化對象或轉換資料
- 對象關系映射
- 基于對象的持久存儲
- 動态轉換使用其他語言編寫的類結構
- 類作為對象
-
:類的繼承關系和調用順序,方法解析順序,Method Resolution Ordercls.__mro__
-
:執行個體調用cls.__class__
屬性時會指向該執行個體對應的類__class__
-
:類、 函數、方法、描述符或生成器對象的名稱cls.__name__
-
:由類的基類組成的元組cls.__bases__
-
:其值是類或函數的限定名稱,即從子產品的全局作用域到類的點分路徑cls.__qualname__
-
:這個方法傳回一個清單,包含類的直接子類cls.__subclasses__()
- 這個方法的實作使用弱引用,防止在超類和子類之間出現循環引用
- 子類在
屬性中儲存指向超類的強引用__bases__
- 這個方法傳回的清單中是記憶體裡現存的子類
-
:建構類時,如果需要擷取儲存在類屬性cls.mro()
中的超類元組,解釋器會調用這個方法;元類可以覆寫這個方法,定制要建構的類解析方法的順序__mro__
-
函數不會列出上述提到的任何一個屬性dir(...)
-
- 小結:
- 元類可以定制類的層次結構。類裝飾器則不同,它隻能影響一個類,而且對後代可能沒有影響
-
:首尾有兩條下劃線的特殊方法和屬性的簡潔讀法(即把dunder
讀成“dunder len”)__len__
- ORM:Object-Relational Mapper(對象關系映射器)
- REPL:read-eval-print loop(讀取-求值-輸出循環)的簡稱
- 綁定方法(bound method):
- 通過執行個體通路的方法會綁定到那個執行個體上
- 方法其實是描述符,通路方法時,會傳回一個包裝自身的對象,把方法綁定到執行個體上
- 那個對象就是綁定方法
- 調用綁定方法時,可以不傳入 self 的值
- 例如,像
這樣指派之後,可以通過my_method = my_obj.method
調用綁定方法my_method()
- 并行指派(parallel assignment):使用類似 a, b = [c, d] 這樣的句法,把可疊代對象中的元素指派給多個變量,也叫解構指派。這是元組拆包的常見用途。
- 初始化方法(initializer):
方法更貼切的名稱(取代構造方法)。__init__
方法的任務是初始化通過 self 參數傳入的執行個體。執行個體其實是由__init__
方法建構的。__new__
- 儲存屬性(storage attribute):托管執行個體中的屬性,用于存儲由描述符管理的屬性的值
- 在 Python 中,None 對象是單例
- 泛函數(generic function):以不同的方式為不同類型的對象實作相同操作的一組函數,
,在其他語言中,這叫多分派方法functools.singledispatch
- 非綁定方法(unbound method):直接通過類通路的執行個體方法沒有綁定到特定的執行個體上,是以把這種方法稱為“非綁定方法”
- 高階函數(higher-order function):以其他函數為參數的函數,例如 sorted、map 和 filter;或者,傳回值為函數的函數,例如 Python 中的裝飾器
- 猴子更新檔(monkey patching):在運作時動态修改子產品、類或函數,通常是添加功能或修正缺陷
- 猴子更新檔在記憶體中發揮作用,不會修改源碼,是以隻對目前運作的程式執行個體有效
- 為猴子更新檔破壞了封裝,而且容易導緻程式與更新檔代碼的實作細節緊密耦合,是以被視為臨時的變通方案,不是內建代碼的推薦方式
- 活性(liveness):異步系統、線程系統或分布式系統在“期待的事情終于發生”(即雖然期待的計算不會立即發生,但最終會完成)時展現出來的特性叫活性。如果系統死鎖了,活性也就沒有了。
- 可散列的(hashable)
- 在散列值永不改變,而且如果 a == b,那麼 hash(a) == hash(b) 也是 True 的情況下,如果對象既有
方法,也有__hash__
方法,那麼這樣的對象稱為可散列的對象__eq__
- 在内置的類型中,大多數不可變的類型都是可散列的
- 但是,僅當元組的每一個元素都是可散列的時,元組才是可散列的
- 在散列值永不改變,而且如果 a == b,那麼 hash(a) == hash(b) 也是 True 的情況下,如果對象既有
- 描述符(descriptor):一個類,實作
、__get__
和__set__
特殊方法中的一個或多個,其執行個體作為另一個類(托管類)的類屬性;描述符管理托管類中托管屬性的存取和删除,資料通常存儲在托管執行個體中__delete__
- 名稱改寫(name mangling):Python 解釋器在運作時自動把私有屬性
重命名為__x
_MyClass__x
- 平坦序列(flat sequence):這種序列類型存儲的是元素的值本身,而不是其他對象的引用
- 内置的類型中, str、bytes、bytearray、memoryview 和 array.array 是平坦序列
- 與之相對應的是容器序列,如:list、tuple 和 collections.deque
- 切片(slicing)
- 使用切片表示法生成序列的子集,例如
my_sequence[2:6]
- 切片經常複制資料,生成新對象
- 然而,
是對整個序列的淺複制my_sequence[:]
- memoryview 對象的切片雖是一個 memoryview 新對象,但會與源對象共享資料
- 使用切片表示法生成序列的子集,例如
- 弱引用(weak reference):一種特殊的對象引用方式,不計入訓示對象的引用計數。弱引用使用 weakref 子產品裡的某個函數和資料結建構立
- 蛇底式(snake_case):辨別符的一種命名約定,使用下劃線(_)連接配接單詞,例如
run_until_complete
- 生成器函數(generator function):定義體中有 yield 關鍵字的函數。調用生成器函數得到的是生成器
- 屬性(attribute):在 Python 中,方法和資料屬性(即 Java 術語中的“字段”)都是屬性。方法也是屬性,隻不過恰好是可調用的對象(通常是函數,但也不一定)
- 特殊方法(special method):名稱特殊的方法,首尾各有兩條下劃線,例如
__getitem__
- 統一通路原則(uniform access principle):Eiffel 語言之父 Bertrand Meyer 寫道:“不管服務是由存儲還是計算實作的,一個子產品提供的所有服務都應該通過統一的方式使用。”
- Python 中,可以使用特性和描述符實作統一通路原則
- 文檔字元串(docstring):documentation string 的簡稱
- 如果子產品、類或函數的第一個語句是字元串字面量,那個字元串會當作所在對象的文檔字元串,解釋器把那個字元串存儲在對象的
屬性中__doc__
- 如果子產品、類或函數的第一個語句是字元串字面量,那個字元串會當作所在對象的文檔字元串,解釋器把那個字元串存儲在對象的
- 虛拟子類(virtual subclass):不繼承自超類,而是使用
注冊的類TheSuperClass.register(TheSubClass)
- 序列化(serialization):把對象在記憶體中的結構轉換成便于存儲或傳輸的二進制或文本格式,而且以後可以在同一個系統或不同的系統中重建對象的副本。pickle 子產品能把任何 Python 對象序列化成二進制格式
- 鴨子類型(duck typing):多态的一種形式,在這種形式中,不管對象屬于哪個類,也不管聲明的具體接口是什麼,隻要對象實作了相應的方法,函數就可以在對象上執行操作
- 一等函數(first-class function):在語言中屬于一等對象的函數(即能在運作時建立,指派給變量,當作參數傳入,以及作為另一個函數的傳回值)。Python 中的函數都是一等函數
- 預激(prime,動詞):在協程上調用
,讓協程向前運作到第一個 yield 表達式,準備好從後續的next(coro)
調用中接收值coro.send(value)
- 裝飾器(decorator)
- 一個可調用的對象 A,傳回另一個可調用的對象 B,在可調用的對象 C 的定義體之前使用句法 @A 調用
- Python 解釋器讀取這樣的代碼時,會調用 A©,把傳回的 B 綁定給之前賦予 C 的變量,也就是把 C的定義體換成 B
- 如果目标可調用對象 C 是函數,那麼 A 是函數裝飾器;如果 C 是類,那麼 A 是類裝飾器
- ☁️ 我的CSDN:https://blog.csdn.net/qq_21579045
- ❄️ 我的部落格園:https://www.cnblogs.com/lyjun/
- ☀️ 我的Github:https://github.com/TinyHandsome
- 🌈 我的bilibili:https://space.bilibili.com/8182822
碌碌謀生,謀其所愛。🌊 @李英俊小朋友