《流暢的 Python 》閱讀筆記
起步
《流暢的python》是一本適合python進階的書, 裡面介紹的基本都是高級的python用法. 對於初學python的人來說, 基礎大概也就夠用了, 但往往由於夠用讓他們忘了深入, 去精通. 我們希望全面了解這個語言的能力邊界, 可能一些高級的特性並不能馬上掌握使用, 因此這本書是工作之餘, 還有餘力的人來閱讀, 我這邊就將其有用, 精妙的進階內容整理出來.
這本書有21個章節, 整理也是根據這些章節過來.
第一章: python數據模型
這部分主要介紹了python的魔術方法, 它們經常是兩個下劃線包圍來命名的(比如 __init__ , __lt__, __len__ ). 這些特殊方法是為了被python解釋器調用的, 這些方法會註冊到他們的類型中方法集合中, 相當於為cpython提供抄近路. 這些方法的速度也比普通方法要快, 當然在自己不清楚這些魔術方法的用途時, 不要隨意添加.
關於字元串的表現形式是兩種, __str__ 與 __repr__ . python的內置函數 repr 就是通過 __repr__ 這個特殊方法來得到一個對象的字元串表示形式. 這個在交互模式下比較常用, 如果沒有實現 __repr__ , 當控制台列印一個對象時往往是 . 而 __str__ 則是 str() 函數時使用的, 或是在 print 函數列印一個對象的時候才被調用, 終端用戶友好.
兩者還有一個區別, 在字元串格式化時, 「%s」 對應了 __str__ . 而 「%r」 對應了 __repr__. __str__ 和 __repr__ 在使用上比較推薦的是,前者是給終端用戶看,而後者則更方便我們調試和記錄日誌.
更多的特殊方法: https://docs.python.org/3/reference/datamodel.html
第二章: 序列構成的數組
這部分主要是介紹序列, 著重介紹數組和元組的一些高級用法.
序列按照容納數據的類型可以分為:
- 容器序列: list、tuple 和 collections.deque 這些序列能存放不同類型的數據
- 扁平序列: str、bytes、bytearray、memoryview 和 array.array,這類序列只能容納一種類型.
如果按照是否能被修改可以分為:
- 可變序列: list、bytearray、array.array、collections.deque 和 memoryview
- 不可變序列: tuple、str 和 bytes
列表推導
列表推導是構建列表的快捷方式, 可讀性更好且效率更高.
例如, 把一個字元串變成unicode的碼位列表的例子, 一般:
Python
symbols = $¢£¥€¤
codes = []
for symbol in symbols:
codes.append(ord(symbol))
使用列表推導:
Python
symbols = $¢£¥€¤
codes = [ord(symbol) for symbol in symbols]
能用列表推導來創建一個列表, 盡量使用推導, 並且保持它簡短.
笛卡爾積與生成器表達式
生成器表達式是能逐個產出元素, 節省內存. 例如:
Python
>>> colors = [black, white]
>>> sizes = [S, M, L]
>>> for tshirt in (%s %s % (c, s) for c in colors for s in sizes):
... print(tshirt)
實例中列表元素比較少, 如果換成兩個各有1000個元素的列表, 顯然這樣組合的笛卡爾積是一個含有100萬元素的列表, 內存將會佔用很大, 而是用生成器表達式就可以幫忙省掉for循環的開銷.
具名元組
元組經常被作為 不可變列表 的代表. 經常只要數字索引獲取元素, 但其實它還可以給元素命名:
Python
>>> from collections import namedtuple
>>> City = namedtuple(City, name country population coordinates)
>>> tokyo = City(Tokyo, JP, 36.933, (35.689722, 139.691667))
>>> tokyo
City(name=Tokyo, country=JP, population=36.933, coordinates=(35.689722,
139.691667))
>>> tokyo.population
36.933
>>> tokyo.coordinates
(35.689722, 139.691667)
>>> tokyo[1]
JP
切片
列表中是以0作為第一個元素的下標, 切片可以根據下標提取某一個片段.
用 s[a:b:c] 的形式對 s 在 a 和 b 之間以 c 為間隔取值。c 的值還可以為負, 負值意味著反向取值.
Python
>>> s = bicycle
>>> s[::3]
bye
>>> s[::-1]
elcycib
>>> s[::-2]
eccb
第三章: 字典和集合
dict 類型不但在各種程序里廣泛使用, 它也是 Python 語言的基石. 正是因為 dict 類型的重要, Python 對其的實現做了高度的優化, 其中最重要的原因就是背後的「散列表」 set(集合)和dict一樣, 其實現基礎也是依賴於散列表.
散列表也叫哈希表, 對於dict類型, 它的key必須是可哈希的數據類型. 什麼是可哈希的數據類型呢, 它的官方解釋是:
如果一個對象是可散列的,那麼在這個對象的生命周期中,它的散列值是不變
的,而且這個對象需要實現 __hash__() 方法。另外可散列對象還要有
__qe__() 方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那麼它們的散列值一定是一樣的……
str, bytes, frozenset 和 數值 都是可散列類型.
字典推導式
Python
DIAL_CODE = [
(86, China),
(91, India),
(7, Russia),
(81, Japan),
]
### 利用字典推導快速生成字典
country_code = {country: code for code, country in DIAL_CODE}
print(country_code)
OUT:
{China: 86, India: 91, Russia: 7, Japan: 81}
defaultdict:處理找不到的鍵的一個選擇
當某個鍵不在映射里, 我們也希望也能得到一個默認值. 這就是 defaultdict , 它是 dict 的子類, 並實現了 __missing__ 方法.
<strong>import collections
index = collections.defaultdict(list)
for item in nums:
key = item % 2
index[key].append(item)</strong>
字典的變種
標準庫里 collections 模塊中,除了 defaultdict 之外的不同映射類型:
- OrderDict: 這個類型在添加鍵的時候,會保存順序,因此鍵的迭代順序總是一致的
- ChainMap: 該類型可以容納數個不同的映射對像,在進行鍵的查找時,這些對象會被當做一個整體逐個查找,直到鍵被找到為止 pylookup = ChainMap(locals(), globals())
- Counter: 這個映射類型會給鍵準備一個整數技術器,每次更行一個鍵的時候都會增加這個計數器,所以這個類型可以用來給散列表對象計數,或者當成多重集來用.
Python
import collections
ct = collections.Counter(abracadabra)
print(ct) # Counter({a: 5, b: 2, r: 2, c: 1, d: 1})
ct.update(aaaaazzz)
print(ct) # Counter({a: 10, z: 3, b: 2, r: 2, c: 1, d: 1})
print(ct.most_common(2)) # [(a, 10), (z, 3)]
- UserDict: 這個類其實就是把標準 dict 用純 Python 又實現了一遍
Python
import collections
class StrKeyDict(collections.UserDict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def __contains__(self, key):
return str(key) in self.data
def __setitem__(self, key, item):
self.data[str(key)] = item
不可變映射類型
說到不可變, 第一想到的肯定是元組, 但是對於字典來說, 要將key和value的對應關係變成不可變, types 模塊的 MappingProxyType 可以做到:
Python
from types import MappingProxyType
d = {1:A}
d_proxy = MappingProxyType(d)
d_proxy[1]=B # TypeError: mappingproxy object does not support item assignment
d[2] = B
print(d_proxy) # mappingproxy({1: A, 2: B})
d_proxy 是動態的, 也就是說對 d 所做的任何改動都會反饋到它上面.
集合論
集合的本質是許多唯一對象的聚集. 因此, 集合可以用於去重. 集合中的元素必須是可散列的, 但是 set 本身是不可散列的, 而 frozenset 本身可以散列.
集合具有唯一性, 與此同時, 集合還實現了很多基礎的中綴運算符. 給定兩個集合 a 和 b, a | b返
回的是它們的合集, a & b 得到的是交集, 而 a – b 得到的是差集.合理的利用這些特性, 不僅能減少代碼的數量, 更能增加運行效率.
Python
# 集合的創建
s = set([1, 2, 2, 3])
# 空集合
s = set()
# 集合字面量
s = {1, 2}
# 集合推導
s = {chr(i) for i in range(23, 45)}
第四章: 文本和位元組序列
本章討論了文本字元串和位元組序列, 以及一些編碼上的轉換. 本章討論的 str 指的是python3下的.
字元問題
字元串是個比較簡單的概念: 一個字元串是一個字元序列. 但是關於 「字元」 的定義卻五花八門, 其中, 「字元」 的最佳定義是 Unicode 字元 . 因此, python3中的 str 對象中獲得的元素就是 unicode 字元.
把碼位轉換成位元組序列的過程就是 編碼, 把位元組序列轉換成碼位的過程就是 編碼 :
Python
>>> s = café
>>> len(s)
4
>>> b = s.encode(utf8)
>>> b
bcafxc3xa9
>>> len(b)
5
>>> b.decode(utf8) #café
碼位可以認為是人類可讀的文本, 而字元序列則可以認為是對機器更友好. 所以要區分 .decode() 和 .encode() 也很簡單. 從位元組序列到人類能理解的文本就是解碼(decode). 而把人類能理解的變成人類不好理解的位元組序列就是編碼(encode).
位元組概要
python3有兩種位元組序列, 不可變的 bytes 類型和可變的 bytearray 類型. 位元組序列中的各個元素都是介於 [0, 255] 之間的整數.
處理編碼問題
python自帶了超過100中編解碼器. 每個編解碼器都有一個名稱, 甚至有的會有一些別名, 如 utf_8 就有 utf8, utf-8, U8 這些別名.
如果字元序列和預期不符, 在進行解碼或編碼時容易拋出 Unicode*Error 的異常. 造成這種錯誤是因為目標編碼中沒有定義某個字元(沒有定義某個碼位對應的字元), 這裡說說解決這類問題的方式.
- 使用python3, python3可以避免95%的字元問題.
- 主流編碼嘗試下: latin1, cp1252, cp437, gb2312, utf-8, utf-16le
- 留意BOM頭部 b』xffxfe』 , UTF-16編碼的序列開頭也會有這幾個額外位元組.
- 找出序列的編碼, 建議使用 codecs 模塊
規範化unicode字元串
Python
s1 = café
s2 = cafu00e9
這兩行代碼完全等價. 而有一種是要避免的是, 在Unicode標準中 é 和 eu0301 這樣的序列叫 「標準等價物」. 這種情況用NFC使用最少的碼位構成等價的字元串:
Python
>>> s1 = café
>>> s2 = cafeu0301
>>> s1, s2
(café, café)
>>> len(s1), len(s2)
(4, 5)
>>> s1 == s2
False
改進後:
Python
>>> from unicodedata import normalize
>>> s1 = café # 把"e"和重音符組合在一起
>>> s2 = cafeu0301 # 分解成"e"和重音符
>>> len(s1), len(s2)
(4, 5)
>>> len(normalize(NFC, s1)), len(normalize(NFC, s2))
(4, 4)
>>> len(normalize(NFD, s1)), len(normalize(NFD, s2))
(5, 5)
>>> normalize(NFC, s1) == normalize(NFC, s2)
True
>>> normalize(NFD, s1) == normalize(NFD, s2)
True
unicode文本排序
對於字元串來說, 比較的碼位. 所以在非 ascii 字元時, 得到的結果可能會不盡人意.
第五章: 一等函數
在python中, 函數是一等對象. 編程語言把 「一等對象」 定義為滿足下列條件:
- 在運行時創建
- 能賦值給變數或數據結構中的元素
- 能作為參數傳給函數
- 能作為函數的返回結果
在python中, 整數, 字元串, 列表, 字典都是一等對象.
把函數視作對象
Python即可以函數式編程,也可以面向對象編程. 這裡我們創建了一個函數, 然後讀取它的 __doc__ 屬性, 並且確定函數對象其實是 function 類的實例:
Python
def factorial(n):
return n
return 1 if n < 2 else n * factorial(n-1)
print(factorial.__doc__)
print(type(factorial))
print(factorial(3))
OUT
return n
<class function>
6
高階函數
高階函數就是接受函數作為參數, 或者把函數作為返回結果的函數. 如 map, filter , reduce等.
比如調用 sorted 時, 將 len 作為參數傳遞:
Python
fruits = [strawberry, fig, apple, cherry, raspberry, banana]
sorted(fruits, key=len)
# [fig, apple, cherry, banana, raspberry, strawberry]
匿名函數
lambda 關鍵字是用來創建匿名函數. 匿名函數一些限制, 匿名函數的定義體只能使用純表達式. 換句話說, lambda 函數內不能賦值, 也不能使用while和try等語句.
Python
fruits = [strawberry, fig, apple, cherry, raspberry, banana]
sorted(fruits, key=lambda word: word[::-1])
# [banana, apple, fig, raspberry, strawberry, cherry]
可調用對象
除了用戶定義的函數, 調用運算符即 () 還可以應用到其他對象上. 如果像判斷對象能否被調用, 可以使用內置的 callable() 函數進行判斷. python的數據模型中有7種可是可以被調用的:
- 用戶定義的函數: 使用def語句或lambda表達式創建
- 內置函數:如len
- 內置方法:如dict.get
- 方法:在類定義體中的函數
- 類
- 類的實例: 如果類定義了 __call__ , 那麼它的實例可以作為函數調用.
- 生成器函數: 使用 yield 關鍵字的函數或方法.
從定位參數到僅限關鍵字參數
就是可變參數和關鍵字參數:
Python
def fun(name, age, *args, **kwargs):
pass
其中 *args 和 **kwargs 都是可迭代對象, 展開後映射到單個參數. args是個元組, kwargs是字典.
第六章: 使用一等函數實現設計模式
雖然設計模式與語言無關, 但這並不意味著每一個模式都能在每一個語言中使用. Gamma 等人合著的 《設計模式:可復用面向對象軟體的基礎》 一書中有 23 個模式, 其中有 16 個在動態語言中」不見了, 或者簡化了」.
這裡不舉例設計模式, 因為書里的模式不常用.
第七章: 函數裝飾器和閉包
函數裝飾器用於在源碼中「標記」函數,以某種方式增強函數的行為。這是一項強大的功
能,但是若想掌握,必須理解閉包。
修飾器和閉包經常在一起討論, 因為修飾器就是閉包的一種形式. 閉包還是回調式非同步編程和函數式編程風格的基礎.
裝飾器基礎知識
裝飾器是可調用的對象, 其參數是另一個函數(被裝飾的函數). 裝飾器可能會處理被
裝飾的函數, 然後把它返回, 或者將其替換成另一個函數或可調用對象.Python
<a href="http://www.jobbole.com/members/decorate">@decorate</a>
def target():
print(running target())
這種寫法與下面寫法完全等價:
Python
def target():
print(running target())
target = decorate(target)
裝飾器是語法糖, 它其實是將函數作為參數讓其他函數處理. 裝飾器有兩大特徵:
- 把被裝飾的函數替換成其他函數
- 裝飾器在載入模塊時立即執行
要理解立即執行看下等價的代碼就知道了, target = decorate(target) 這句調用了函數. 一般情況下裝飾函數都會將某個函數作為返回值.
變數作用域規則
要理解裝飾器中變數的作用域, 應該要理解閉包, 我覺得書里將閉包和作用域的順序換一下比較好. 在python中, 一個變數的查找順序是 LEGB (L:Local 局部環境,E:Enclosing 閉包,G:Global 全局,B:Built-in 內建).
Python
base = 20
def get_compare():
base = 10
def real_compare(value):
return value > base
return real_compare
compare_10 = get_compare()
print(compare_10(5))
在閉包的函數 real_compare 中, 使用的變數 base 其實是 base = 10 的. 因為base這個變數在閉包中就能命中, 而不需要去 global 中獲取.
閉包
閉包其實挺好理解的, 當匿名函數出現的時候, 才使得這部分難以掌握. 簡單簡短的解釋閉包就是:
名字空間與函數捆綁後的結果被稱為一個閉包(closure).
這個名字空間就是 LEGB 中的 E . 所以閉包不僅僅是將函數作為返回值. 而是將名字空間和函數捆綁後作為返回值的. 多少人忘了理解這個 「捆綁」 , 不知道變數最終取的哪和哪啊. 哎.
標準庫中的裝飾器
python內置了三個用於裝飾方法的函數: property 、 classmethod 和 staticmethod .
這些是用來豐富類的.Python
class A(object):
@property
def age():
return 12
第八章: 對象引用、可變性和垃圾回收
變數不是盒子
很多人把變數理解為盒子, 要存什麼數據往盒子里扔就行了.
Python
a = [1,2,3]
b = a
a.append(4)
print(b) # [1, 2, 3, 4]
變數 a 和 b 引用同一個列表, 而不是那個列表的副本. 因此賦值語句應該理解為將變數和值進行引用的關係而已.
標識、相等性和別名
要知道變數a和b是否是同一個值的引用, 可以用 is 來進行判斷:
Python
>>> a = b = [4,5,6]
>>> c = [4,5,6]
>>> a is b
True
>>> x is c
False
如果兩個變數都是指向同一個對象, 我們通常會說變數是另一個變數的 別名 .
在==和is之間選擇
運算符 == 是用來判斷兩個對象值是否相等(注意是對象值). 而 is 則是用於判斷兩個變數是否指向同一個對象, 或者說判斷變數是不是兩一個的別名, is 並不關心對象的值. 從使用上, == 使用比較多, 而 is 的執行速度比較快.默認做淺複製
Python
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1) # 通過構造方法進行複製
l2 = l1[:] #也可以這樣想寫
>>> l2 == l1
True
>>> l2 is l1
False
儘管 l2 是 l1 的副本, 但是複製的過程是先複製(即複製了最外層容器,副本中的元素是源容器中元素的引用). 因此在操作 l2[1] 時, l1[1] 也會跟著變化. 而如果列表中的所有元素是不可變的, 那麼就沒有這樣的問題, 而且還能節省內存. 但是, 如果有可變元素存在, 就可能造成意想不到的問題.
python標準庫中提供了兩個工具 copy 和 deepcopy . 分別用於淺拷貝與深拷貝:
Python
import copy
l1 = [3, [55, 44], (7, 8, 9)]
l2 = copy.copy(l1)
l2 = copy.deepcopy(l1)
函數的參數做引用時
python中的函數參數都是採用共享傳參. 共享傳參指函數的各個形式參數獲得實參中各個引用的副本. 也就是說, 函數內部的形參
是實參的別名.這種方案就是當傳入參數是可變對象時, 在函數內對參數的修改也就是對外部可變對象進行修改. 但這種參數試圖重新賦值為一個新的對象時則無效, 因為這只是相當於把參數作為另一個東西的引用, 原有的對象並不變. 也就是說, 在函數內, 參數是不能把一個對象替換成另一個對象的.
不要使用可變類型作為參數的默認值
參數默認值是個很棒的特性. 對於開發者來說, 應該避免使用可變對象作為參數默認值. 因為如果參數默認值是可變對象, 而且修改了它的內容, 那麼後續的函數調用上都會收到影響.
del和垃圾回收
在python中, 當一個對象失去了最後一個引用時, 會當做垃圾, 然後被回收掉. 雖然python提供了 del 語句用來刪除變數. 但實際上只是刪除了變數和對象之間的引用, 並不一定能讓對象進行回收, 因為這個對象可能還存在其他引用.
在CPython中, 垃圾回收主要用的是引用計數的演算法. 每個對象都會統計有多少引用指向自己. 當引用計數歸零時, 意味著這個對象沒有在使用, 對象就會被立即銷毀.
符合Python風格的對象
得益於 Python 數據模型,自定義類型的行為可以像內置類型那樣自然。實現如此自然的
行為,靠的不是繼承,而是鴨子類型(duck typing):我們只需按照預定行為實現對象所
需的方法即可。對象表示形式
每門面向對象的語言至少都有一種獲取對象的字元串表示形式的標準方式。Python 提供了
兩種方式。- repr() : 以便於開發者理解的方式返回對象的字元串表示形式。
- str() : 以便於用戶理解的方式返回對象的字元串表示形式。
classmethod 與 staticmethod
這兩個都是python內置提供了裝飾器, 一般python教程都沒有提到這兩個裝飾器. 這兩個都是在類 class 定義中使用的, 一般情況下, class 裡面定義的函數是與其類的實例進行綁定的. 而這兩個裝飾器則可以改變這種調用方式.
先來看看 classmethod , 這個裝飾器不是操作實例的方法, 並且將類本身作為第一個參數. 而 staticmethod 裝飾器也會改變方法的調用方式, 它就是一個普通的函數,
classmethod 與 staticmethod 的區別就是 classmethod 會把類本身作為第一個參數傳入, 其他都一樣了.
看看例子:
Python
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args
... @staticmethod
... def statmeth(*args):
... return args
...
>>> Demo.klassmeth()
(<class __main__.Demo>,)
>>> Demo.klassmeth(spam)
(<class __main__.Demo>, spam)
>>> Demo.statmeth()
()
>>> Demo.statmeth(spam)
(spam,)
格式化顯示
內置的 format() 函數和 str.format() 方法把各個類型的格式化方式委託給相應的 .__format__(format_spec) 方法. format_spec 是格式說明符,它是:
- format(my_obj, format_spec) 的第二個參數
- str.format() 方法的格式字元串,{} 里代換欄位中冒號後面的部分
Python的私有屬性和」受保護的」屬性
python中對於實例變數沒有像 private 這樣的修飾符來創建私有屬性, 在python中, 有一個簡單的機制來處理私有屬性.
Python
class A:
def __init__(self):
self.__x = 1
a = A()
print(a.__x) # AttributeError: A object has no attribute __x
print(a.__dict__)
如果屬性以 __name 的 兩個下劃線為前綴, 尾部最多一個下劃線 命名的實例屬性, python會把它名稱前面加一個下劃線加類名, 再放入 __dict__ 中, 以 __name 為例, 就會變成 _A__name .
名稱改寫算是一種安全措施, 但是不能保證萬無一失, 它能避免意外訪問, 但不能阻止故意做壞事.
只要知道私有屬性的機制, 任何人都能直接讀取和改寫私有屬性. 因此很多python程序員嚴格規定: 遵守使用一個下劃線標記對象的私有屬性 . Python 解釋器不會對使用單個下劃線的屬性名做特殊處理, 由程序員自行控制, 不在類外部訪問這些屬性. 這種方法也是所推薦的, 兩個下劃線的那種方式就不要再用了. 引用python大神的話:
絕對不要使用兩個前導下劃線,這是很煩人的自私行為。如果擔心名稱衝突,應該明
確使用一種名稱改寫方式(如 _MyThing_blahblah)。這其實與使用雙下劃線一
樣,不過自己定的規則比雙下劃線易於理解。Python中的把使用一個下劃線前綴標記的屬性稱為」受保護的」屬性
使用 slots 類屬性節省空間
默認情況下, python在各個實例中, 用 __dict__ 的字典存儲實例屬性. 因此實例的屬性是動態變化的, 可以在運行期間任意添加屬性. 而字典是消耗內存比較大的結構. 因此當對象的屬性名稱確定時, 使用 __slots__ 可以節約內存.
Python
class Vector2d:
__slots__ = (__x, __y)
typecode = d
# 下面是各個方法(因排版需要而省略了)
在類中定義 __slots__ 屬性的目的是告訴解釋器:」這個類中的所有實例屬性都在這兒
了!」 這樣, Python 會在各個實例中使用類似元組的結構存儲實例變數, 從而避免使用消耗內存的 __dict__ 屬性. 如果有數百萬個實例同時活動, 這樣做能節省大量內存.第十章: 序列的修改、散列和切片
協議和鴨子類型
在python中, 序列類型不需要使用繼承, 只需要符合序列協議的方法即可. 這裡的協議就是實現 __len__ 和 __getitem__ 兩個方法. 任何類, 只要實現了這兩個方法, 它就滿足了序列操作, 因為它的行為像序列.
協議是非正式的, 沒有強制力, 因此你知道類的具體使用場景, 通常只要實現一個協議的部分. 例如, 為了支持迭代, 只需實現 __getitem__ 方法, 沒必要提供 __len__ 方法, 這也就解釋了 鴨子類型 :
當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,
那麼這隻鳥就可以被稱為鴨子
可切片的序列
切片(Slice)是用來獲取序列某一段範圍的元素. 切片操作也是通過 __getitem__ 來完成的:
Python
class Vector:
# 省略了很多行
# ...
def __len__(self):
return len(self._components)
# 省略了很多
def __getitem__(self, index):
cls = type(self) # 獲取實例的類型
if isinstance(index, slice): # 如果index參數值是切片的對象
# 調用Vector的構造方法,建立一個新的切片後的Vector類
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))
動態存取屬性
通過訪問分量名來獲取屬性:
Python
shortcut_names = xyzt
def __getattr__(self, name):
cls = type(self) # 獲取類型
if len(name) == 1: # 判斷屬性名是否在我們定義的names中
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
msg = {} objects has no attribute {}
raise AttributeError(msg.format(cls, name))
test = Vector([3, 4, 5])
print(test.x)
print(test.y)
print(test.z)
print(test.c)
散列和快速等值測試
實現 __hash__ 方法。加上現有的 __eq__ 方法,這會把實例變成可散列的對象.
當序列是多維是時候, 我們有一個效率更高的方法:
Python
def __eq__(self, other):
if len(self) != len(other): # 首先判斷長度是否相等
return False
for a, b in zip(self, other): # 接著逐一判斷每個元素是否相等
if a != b:
return False
return True
# 我們也可以寫的漂亮點
def __eq__(self, other):
return (len(self) == len(other)) and all(a == b for a, b in zip(self, other))
第十一章: 介面:從協議到抽象基類
這些協議定義為非正式的介面, 是讓編程語言實現多態的方式. 在python中, 沒有 interface關鍵字, 而且除了抽象基類, 每個類都有介面: 所有類都可以自行實現 __getitem__ 和 __add__ .
有寫規定則是程序員在開發過程中慢慢總結出來的, 如受保護的屬性命名採用單個前導下劃線, 還有一些編碼規範之類的.
協議是介面, 但不是正式的, 這些規定並不是強制性的, 一個類可能只實現部分介面, 這是允許的.
既然有非正式的協議, 那麼有沒有正式的協議呢? 有, 抽象基類就是一種強制性的協議.
抽象基類要求其子類需要實現定義的某個介面, 且抽象基類不能實例化.
Python文化中的介面和協議
引入抽象基類之前, python就已經非常成功了, 即使現在也很少使用抽象基類. 通過鴨子類型和協議, 我們把協議定義為非正式介面, 是讓python實現多態的方式.
另一邊面, 不要覺得把公開數據屬性放入對象的介面中不妥, 如果需要, 總能實現讀值和設值方法, 把數據屬性變成特性. 對象公開方法的自己, 讓對象在系統中扮演特定的角色. 因此, 介面是實現特定角色的方法集合.
序列協議是python最基礎的協議之一, 即便對象只實現那個協議最基本的一部分, 解釋器也會負責地處理.
水禽和抽象基類
鴨子類型在很多情況下十分有用, 但是隨著發展, 通常由了更好的方式.
近代, 屬和種基本是根據表型系統學分類的, 鴨科屬於水禽, 而水禽還包括鵝, 鴻雁等. 水禽是對某一類表現一致進行的分類, 他們有一些統一」描述」部分.
因此, 根據分類的演化, 需要有個水禽類型, 只要 cls 是抽象基類, 即 cls 的元類是 abc.ABCMeta , 就可以使用 isinstance(obj, cls) 來進行判斷.
與具類相比, 抽象基類有很多理論上的優點, 被註冊的類必須滿足抽象基類對方法和簽名的要求, 更重要的是滿足底層語義契約.
標準庫中的抽象基類
大多數的標準庫的抽象基類在 collections.abc 模塊中定義. 少部分在 numbers 和 io 包中有一些抽象基類. 標準庫中有兩個 abc 模塊, 這裡只討論 collections.abc .
這個模塊中定義了 16 個抽象基類.
Iterable、Container 和 Sized
各個集合應該繼承這三個抽象基類,或者至少實現兼容的協議。Iterable 通過 __iter__ 方法支持迭代,Container 通過 __contains__ 方法支持 in 運算符,Sized通過 __len__ 方法支持 len() 函數。Sequence、Mapping 和 Set
這三個是主要的不可變集合類型,而且各自都有可變的子類。MappingView
在 Python3 中,映射方法 .items()、.keys() 和 .values() 返回的對象分別是ItemsView、KeysView 和 ValuesView 的實例。前兩個類還從 Set 類繼承了豐富的接口。Callable 和 Hashable
這兩個抽象基類與集合沒有太大的關係,只不過因為 collections.abc 是標準庫中定義抽象基類的第一個模塊,而它們又太重要了,因此才把它們放到 collections.abc模塊中。我從未見過 Callable 或 Hashable 的子類。這兩個抽象基類的主要作用是為內置函數 isinstance 提供支持,以一種安全的方式判斷對象能不能調用或散列。Iterator
注意它是 Iterable 的子類。第十二章: 繼承的優缺點
很多人覺得多重繼承得不償失, 那些不支持多繼承的編程語言好像也沒什麼損失.
子類化內置類型很麻煩
python2.2 以前, 內置類型(如list, dict)是不能子類化的. 它們是不能被其他類所繼承的, 原因是內置類型是C語言實現的, 不會調用用戶定義的類覆蓋的方法.
至於內置類型的子類覆蓋的方法會不會隱式調用, CPython 官方也沒有制定規則. 基本上, 內置類型的方法不會調用子類覆蓋的方法. 例如, dict 的子類覆蓋的 __getitem__ 方法不會覆蓋內置類型的 get() 方法調用.
多重繼承和方法解析順序
任何實現多重繼承的語言都要處理潛在的命名衝突,這種衝突由不相關的祖先類實現同名
方法引起。這種衝突稱為「菱形問題」,如圖.Python 會按照特定的順序遍歷繼承
圖。這個順序叫方法解析順序(Method Resolution Order,MRO)。類都有一個名為mro 的屬性,它的值是一個元組,按照方法解析順序列出各個超類,從當前類一直向上,直到 object 類。第十三章: 正確重載運算符
在python中, 大多數的運算符是可以重載的, 如 == 對應了 __eq__ , + 對應 __add__ .
某些運算符不能重載, 如 is, and, or, and.
第十四章: 可迭代的對象、迭代器和生成器
迭代是數據處理的基石. 掃描內存中放不下的數據集時, 我們要找到一種惰性獲取數據的方式, 即按需一次獲取一個數據. 這就是 迭代器模式 .
python中有 yield 關鍵字, 用於構建 生成器(generator), 其作用用於迭代器一樣.
所有的生成器都是迭代器, 因為生成器完全實現了迭代器的介面.
檢查對象 x 是否迭代, 最準確的方法是調用 iter(x) , 如果不可迭代, 則拋出 TypeError 異常. 這個方法比 isinstance(x, abc.Iterable) 更準確, 因為它還考慮到遺留的 __getitem__ 方法.
可迭代的對象與迭代器的對比
我們需要對可迭代的對象進行一下定義:
使用 iter 內置函數可以獲取迭代器的對象。如果對象實現了能返回迭代器的
iter 方法,那麼對象就是可迭代的。序列都可以迭代;實現了 getitem 方
法,而且其參數是從零開始的索引,這種對象也可以迭代。我們要明確可迭代對象和迭代器之間的關係: 從可迭代的對象中獲取迭代器.
標準的迭代器介面有兩個方法:
- __next__: 返回下一個可用的元素, 如果沒有元素了, 拋出 StopIteration 異常.
- __iter__: 返回 self , 以便咋應該使用可迭代對象的地方使用迭代器.
典型的迭代器
為了清楚地說明可迭代對象與迭代器之間的重要區別, 我們將兩者分開, 寫成兩個類:
Python
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 # 索引+1
return word # 返回單詞
def __iter__(self):
return self # 返回self
這個例子主要是為了區分可迭代對象和迭代器, 這種情況工作量一般比較大, 程序員也不願這樣寫.
構建可迭代對象和迭代器經常會出現錯誤, 原因是混淆了二者. 要知道, 可迭代的對象有個 __iter__ 方法, 每次都實例化一個新的迭代器; 而迭代器是要實現 __next__ 方法, 返回單個元素, 同時還要提供 __iter__ 方法返回迭代器本身.
可迭代對象一定不能是自身的迭代器. 也就是說, 可迭代對象必須實現 __iter__ 方法, 但不能實現 __next__ 方法.
小結下, 迭代器可以迭代, 但是可迭代對象不是迭代器.
生成器函數
實現相同功能, 覆蓋python習慣的方式, 就是用生成器代替迭代器 SentenceIterator . 將上個例子改成生成器的方式:
Python
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: # 迭代實例的words
yield word # 生成單詞
return
在這個例子中, 迭代器其實就是生成器對象, 每次調用 __iter__ 都會自動創建, 因為這裡的 __iter__ 方法是生成器函數.
生成器函數的工作原理
只要python函數的定義體中有 yield 關鍵字, 改函數就是生成器函數. 調用生成器函數時, 會返回一個生成器對象. 也就是說, 生成器函數是生成器工廠.普通函數與生成器函數的唯一區別就是, 生成器函數裡面有 yield 關鍵字.
生成器函數會創建一個生成器對象, 包裝生成器函數的定義體. 吧生成器傳給 next(…) 函數時, 生成器函數會向前, 執行函數體中下一個 yield 語句, 返回產出的值, 並在函數定義體的當前位置暫停.
惰性實現
惰性的方式就是索性把所有數據都產出, 這是區別於 next(…) 一次生成一次元素的.
Python
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 match in RE_WORD.finditer(self.text):
yield match.group()
生成器表達式
生成器表達式可以理解為列表推導的惰性版本: 不會迫切地構建列表, 而是返回一個生成器, 按需惰性生成元素. 也就是, 如果列表推導是產出列表的工廠, 那麼生成器表達式就是產出生成器的工廠.
Python
def gen_AB():
print(start)
yield A
print(continue)
yield B
print(end.)
res1 = [x*3 for x in gen_AB()]
for i in res1:
print(-->, i)
可以看出, 生成器表達式會產出生成器, 因此可以使用生成器表達式減少代碼:
Python
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 (match.group() for match in RE_WORD.finditer(self.text))
這裡的 __iter__ 不是生成器函數了, 而是使用生成器表達式構建生成器, 最終的效果一樣. 調用 __iter__ 方法會得到一個生成器對象.
生成器表達式是語法糖, 完全可以替換生成器函數.
標準庫中的生成器函數
標準庫提供了很多生成器, 有用於逐行迭代純文本文件的對象, 還有出色的 os.walk 函數. 這個函數在遍歷目錄樹的過程中產出文件名, 因此遞歸搜索文件系統像 for 循環那樣簡單.
標準庫中的生成器大多在 itertools 和 functools 中, 表格中不代表所有.
用於過濾的生成器函數
模塊函數說明itertoolscompress(it, selector_it)並行處理兩個可迭代的對象;如果 selector_it 中的元素是真值,產出 it 中對應的元素itertoolsdropwhile(predicate, it)處理 it,跳過 predicate 的計算結果為真值的元素,然後產出剩下的各個元素(不再進一步檢查)(內置)filter(predicate, it)把 it 中的各個元素傳給 predicate,如果 predicate(item) 返回真值,那麼產出對應的元素;如果 predicate 是 None,那麼只產出真值元素
用於映射的生成器函數
模塊函數說明itertoolsaccumulate(it, [func])產出累積的總和;如果提供了 func,那麼把前兩個元素傳給它,然後把計算結果和下一個元素傳給它,以此類推,最後產出結果(內置)enumerate(iterable, start=0)產出由兩個元素組成的元組,結構是 (index, item),其中 index 從 start 開始計數,item 則從 iterable 中獲取(內置)map(func, it1, [it2, …, itN])把 it 中的各個元素傳給func,產出結果;如果傳入 N 個可迭代的對象,那麼 func 必須能接受 N 個參數,而且要並行處理各個可迭代的對象
合併多個可迭代對象的生成器函數
模塊函數說明itertoolschain(it1, …, itN)先產出 it1 中的所有元素,然後產出 it2 中的所有元素,以此類推,無縫連接在一起itertoolschain.from_iterable(it)產出 it 生成的各個可迭代對象中的元素,一個接一個,無縫連接在一起;it 應該產出可迭代的元素,例如可迭代的對象列表(內置)zip(it1, …, itN)並行從輸入的各個可迭代對象中獲取元素,產出由 N 個元素組成的元組,只要有一個可迭代的對象到頭了,就默默地停止
新的句法:yield from
如果生成器函數需要產出另一個生成器生成的值, 傳統的方式是嵌套的 for 循環, 例如, 我們要自己實現 chain 生成器:
Python
>>> def chain(*iterables):
... for it in iterables:
... for i in it:
... yield i
...
>>> s = ABC
>>> t = tuple(range(3))
>>> list(chain(s, t))
[A, B, C, 0, 1, 2]
chain 生成器函數把操作依次交給接收到的可迭代對象處理. 而改用 yield from 語句可以簡化:
Python
>>> def chain(*iterables):
... for i in iterables:
... yield from i
...
>>> list(chain(s, t))
[A, B, C, 0, 1, 2]
可以看出, yield from i 取代一個 for 循環. 並且讓代碼讀起來更流暢.
可迭代的歸約函數
有些函數接受可迭代對象, 但僅返回單個結果, 這類函數叫規約函數.
模塊函數說明(內置)sum(it, start=0)it 中所有元素的總和,如果提供可選的 start,會把它加上(計算浮點數的加法時,可以使用 math.fsum 函數提高精度)(內置)all(it)it 中的所有元素都為真值時返回 True,否則返回 False;all([]) 返回 True(內置)any(it)只要 it 中有元素為真值就返回 True,否則返回 False;any([]) 返回 False(內置)max(it, [key=,] [default=])返回 it 中值最大的元素;*key 是排序函數,與 sorted 函數中的一樣;如果可迭代的對象為空,返回 defaultfunctoolsreduce(func, it, [initial])把前兩個元素傳給 func,然後把計算結果和第三個元素傳給 func,以此類推,返回最後的結果;如果提供了 initial,把它當作第一個元素傳入
第十五章: 上下文管理器和 else 塊
本章討論的是其他語言不常見的流程式控制制特性, 正因如此, python新手往往忽視或沒有充分使用這些特性. 下面討論的特性有:
- with 語句和上下文管理器
- for while try 語句的 else 子句
with 語句會設置一個臨時的上下文, 交給上下文管理器對象控制, 並且負責清理上下文. 這麼做能避免錯誤並減少代碼量, 因此API更安全, 而且更易於使用. 除了自動關閉文件之外, with 塊還有其他很多用途.
else 子句先做這個,選擇性再做那個的作用.
if語句之外的else塊
這裡的 else 不是在在 if 語句中使用的, 而是在 for while try 語句中使用的.
Python
for i in lst:
if i > 10:
break
else:
print("no num bigger than 10")
else 子句的行為如下:
- for : 僅當 for 循環運行完畢時(即 for 循環沒有被 break 語句中止)才運行 else 塊。
- while : 僅當 while 循環因為條件為假值而退出時(即 while 循環沒有被 break 語句中止)才運行 else 塊。
- try : 僅當 try 塊中沒有異常拋出時才運行 else 塊。
在所有情況下, 如果異常或者 return , break 或 continue 語句導致控制權跳到了複合語句的住塊外, else 子句也會被跳過.
這一些情況下, 使用 else 子句通常讓代碼更便於閱讀, 而且能省去一些麻煩, 不用設置控制標誌作用的變數和額外的if判斷.
上下文管理器和with塊
上下文管理器對象的目的就是管理 with 語句, with 語句的目的是簡化 try/finally 模式. 這種模式用於保證一段代碼運行完畢後執行某項操作, 即便那段代碼由於異常, return 或者 sys.exit() 調用而終止, 也會執行執行的操作. finally 子句中的代碼通常用於釋放重要的資源, 或者還原臨時變更的狀態.
上下文管理器協議包含 __enter__ 和 __exit__ 兩個方法. with 語句開始運行時, 會在上下文管理器上調用 __enter__ 方法, 待 with 語句運行結束後, 再調用 __exit__ 方法, 以此扮演了 finally 子句的角色.
with 最常見的例子就是確保關閉文件對象.
上下文管理器調用 __enter__ 沒有參數, 而調用 __exit__ 時, 會傳入3個參數:
- exc_type : 異常類(例如 ZeroDivisionError)
- exc_value : 異常實例。有時會有參數傳給異常構造方法,例如錯誤消息,這些參數可以使用 exc_value.args 獲取
- traceback : traceback 對象
contextlib模塊中的實用工具
在ptyhon的標準庫中, contextlib 模塊中還有一些類和其他函數,使用範圍更廣。
- closing: 如果對象提供了 close() 方法,但沒有實現 __enter__/__exit__ 協議,那麼可以使用這個函數構建上下文管理器。
- suppress: 構建臨時忽略指定異常的上下文管理器。
- @contextmanager: 這個裝飾器把簡單的生成器函數變成上下文管理器,這樣就不用創建類去實現管理器協議了。
- ContextDecorator: 這是個基類,用於定義基於類的上下文管理器。這種上下文管理器也能用於裝飾函數,在受管理的上下文中運行整個函數。
- ExitStack: 這個上下文管理器能進入多個上下文管理器。with 塊結束時,ExitStack 按照後進先出的順序調用棧中各個上下文管理器的 __exit__ 方法。如果事先不知道 with 塊要進入多少個上下文管理器,可以使用這個類。例如,同時打開任意一個文件列表中的所有文件。
顯然,在這些實用工具中,使用最廣泛的是 @contextmanager 裝飾器,因此要格外留心。這個裝飾器也有迷惑人的一面,因為它與迭代無關,卻要使用 yield 語句。
使用@contextmanager
@contextmanager 裝飾器能減少創建上下文管理器的樣板代碼量, 因為不用定義 __enter__和 __exit__ 方法, 只需要實現一個 yield 語句的生成器.
Python
import sys
import contextlib
@contextlib.contextmanager
def looking_glass():
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
with looking_glass() as f:
print(f) # YKCOWREBBAJ
print("ABCD") # DCBA
yield 語句起到了分割的作用, yield 語句前面的所有代碼在 with 塊開始時(即解釋器調用 __enter__ 方法時)執行, yield 語句後面的代碼在 with 塊結束時(即調用 __exit__ 方法時)執行.
第十六章: 協程
為了理解協程的概念, 先從 yield 來說. yield item 會產出一個值, 提供給 next(…) 調用方; 此外還會做出讓步, 暫停執行生成器, 讓調用方繼續工作, 直到需要使用另一個值時再調用 next(…) 從暫停的地方繼續執行.
從句子語法上看, 協程與生成器類似, 都是通過 yield 關鍵字的函數. 可是, 在協程中, yield 通常出現在表達式的右邊(datum = yield), 可以產出值, 也可以不產出(如果yield後面沒有表達式, 那麼會產出None). 協程可能會從調用方接收數據, 不過調用方把數據提供給協程使用的是 .send(datum) 方法. 而不是 next(…) . 通常, 調用方會把值推送給協程.
生成器調用方是一直索要數據, 而協程這是調用方可以想它傳入數據, 協程也不一定要產出數據.
不管數據如何流動, yield 都是一種流程式控制制工具, 使用它可以實現寫作式多任務: 協程可以把控制器讓步給中心調度程序, 從而激活其他的協程.
生成器如何進化成協程
協程的底層框架實現後, 生成器API中增加了 .send(value) 方法. 生成器的調用方可以使用 .send(…) 來發送數據, 發送的數據會變成生成器函數中 yield 表達式的值. 因此, 生成器可以作為協程使用. 除了 .send(…) 方法, 還添加了 .throw(…) 和 .close() 方法, 用來讓調用方拋出異常和終止生成器.
用作協程的生成器的基本行為
Python
>>> def simple_coroutine():
... print(-> coroutine started)
... x = yield
... print(-> coroutine received:, x)
...
>>> my_coro = simple_coroutine()
>>> my_coro
<generator object simple_coroutine at 0x100c2be10>
>>> next(my_coro)
-> coroutine started
>>> my_coro.send(42)
-> coroutine received: 42
Traceback (most recent call last):
...
StopIteration
在 yield 表達式中, 如果協程只需從調用那接受數據, 那麼產出的值是 None . 與創建生成器的方式一樣, 調用函數得到生成器對象. 協程都要先調用 next(…) 函數, 因為生成器還沒啟動, 沒在 yield 出暫定, 所以一開始無法發送數據. 如果控制器流動到協程定義體末尾, 會像迭代器一樣拋出 StopIteration 異常.
使用協程的好處是不用加鎖, 因為所有協程只在一個線程中運行, 他們是非搶佔式的. 協程也有一些狀態, 可以調用 inspect.getgeneratorstate(…) 來獲得, 協程都是這4個狀態中的一種:
- 『GEN_CREATED』 等待開始執行。
- 『GEN_RUNNING』 解釋器正在執行。
- 『GEN_SUSPENDED』 在 yield 表達式處暫停。
- 『GEN_CLOSED』 執行結束。
只有在多線程應用中才能看到這個狀態。此外,生成器對象在自己身上調用 getgeneratorstate 函數也行,不過這樣做沒什麼用。
為了更好理解繼承的行為, 來看看產生兩個值的協程:
Python
>>> from inspect import getgeneratorstate
>>> def simple_coro2(a):
... print(-> Started: a =, a)
... b = yield a
... print(-> Received: b =, b)
... c = yield a + b
... print(-> Received: c =, c)
...
>>> my_coro2 = simple_coro2(14)
>>> getgeneratorstate(my_coro2) # 協程處於未啟動的狀態
GEN_CREATED
>>> next(my_coro2) # 向前執行到yield表達式, 產出值 a, 暫停並等待 b 賦值
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2) # 協程處於暫停狀態
GEN_SUSPENDED
>>> my_coro2.send(28) # 數字28發給協程, yield 表達式中 b 得到28, 協程向前執行, 產出 a + b 值
-> Received: b = 28
42
>>> my_coro2.send(99) # 同理, c 得到 99, 但是由於協程終止, 導致生成器對象拋出 StopIteration 異常
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2) # 協程處於終止狀態
GEN_CLOSED
關鍵的一點是, 協程在 yield 關鍵字所在的位置暫停執行. 對於 b = yield a 這行代碼來說, 等到客戶端代碼再激活協程時才會設定 b 的值. 這種方式要花點時間才能習慣, 理解了這個, 才能弄懂非同步編程中 yield 的作用. 對於實例的代碼中函數 simple_coro2 的執行過程可以分為三個階段:
示例:使用協程計算移動平均值
Python
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total/count
這是一個動態計算平均值的協程代碼, 這個無限循環表明, 它會一直接收值然後生成結果. 只有當調用方在協程上調用 .close() 方法, 或者沒有該協程的引用時, 協程才會終止.
協程的好處是, 無需使用實例屬性或者閉包, 在多次調用之間都能保持上下文.
預激協程的裝飾器
如果沒有執行 next(…) , 協程沒什麼用. 為了簡化協程的用法, 有時會使用一個預激裝飾器.
Python
from functools import wraps
def coroutine(func):
"""裝飾器:向前執行到第一個`yield`表達式,預激`func`"""
@wraps(func)
def primer(*args,**kwargs): # 調用 primer 函數時,返回預激後的生成器
gen = func(*args,**kwargs) # 調用被裝飾的函數,獲取生成器對象。
next(gen) # 預激生成器
return gen # 返回生成器
return primer
終止協程和異常處理
協程中未處理的異常會向上冒泡, 傳給 next() 函數或者 send() 的調用方. 如果這個異常沒有處理, 會導致協程終止.
Python
>>> coro_avg.send(40)
40.0
>>> coro_avg.send(50)
45.0
>>> coro_avg.send(spam) # 傳入會產生異常的值
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +=: float and str
>>> coro_avg.send(60)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
這要求在協程內部要處理這些異常, 另外, 客戶端代碼也可以顯示的發送異常給協程, 方法是 throw 和 close :
Python
1coro_avg.throw(ZeroDivisionError)
協程內部如果不能處理這個異常, 就會導致協程終止.
而 close 是致使在暫停的 yield 表達式處拋出 GeneratorExit 異常. 協程內部當然允許處理這個異常, 但收到這個異常時一定不能產出值, 不然解釋器會拋出 RuntimeError 異常.
讓協程返回值
Python
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 (count, average)
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
try:
coro_avg.send(None) # 發送 None 讓協程終止
except StopIteration as exc:
result = exc.value
為了返回值, 協程必須正常終止, 而正常終止的的協程會拋出 StopIteration 異常, 因此需要調用方處理這個異常.
使用yield from
yield from 是全新的語法結構. 它的作用比 yield 多很多.
Python
>>> def gen():
... for c in AB:
... yield c
... for i in range(1, 3):
... yield i
...
>>> list(gen())
[A, B, 1, 2]
可以改寫成:
Python
>>> def gen():
... yield from AB
... yield from range(1, 3)
...
>>> list(gen())
[A, B, 1, 2]
在生成器 gen 中使用 yield form subgen() 時, subgen 會得到控制權, 把產出的值傳給 gen 的調用方, 既調用方可以直接調用 subgen. 而此時, gen 會阻塞, 等待 subgen 終止.
yield from x 表達式對 x 對象所做的第一件事是, 調用 iter(x) 獲得迭代器. 因此, x 對象可以是任何可迭代對象.
這個語義過於複雜, 來看看作者 Greg Ewing 的解釋:
「把迭代器當作生成器使用,相當於把子生成器的定義體內聯在 yield from 表達式
中。此外,子生成器可以執行 return 語句,返回一個值,而返回的值會成為 yield
from 表達式的值。」子生成器是從 yield from 中獲得的生成器. 而後, 如果調用方使用 send() 方法, 其實也是直接傳給子生成器. 如果發送的是 None , 那麼會調用子生成器的 __next__() 方法. 如果不是 None , 那麼會調用子生成器的 send() 方法. 當子生成器拋出 StopIteration 異常, 那麼委派生成器恢復運行. 任何其他異常都會向上冒泡, 傳給委派生成器.
生成器在 return expr 表達式中會觸發 StopIteration 異常.
第十七章: 使用期物處理並發
「期物」 是什麼概念呢? 期物指一種對象, 表示非同步執行的操作. 這個概念是 concurrent.futures 模塊和 asyncio 包的基礎.
示例:網路下載的三種風格
為了高效處理網路io, 需要使用並發, 因為網路有很高的延遲, 所以為了不浪費 CPU 周期去等待.
以一個下載網路 20 個圖片的程序看, 串列下載的耗時 7.18s . 多線程的下載耗時 1.40s, asyncio的耗時 1.35s . 兩個並發下載的腳本之間差異不大, 當對於串列的來說, 快了很多.
阻塞型I/O和GIL
CPython解釋器不是線程安全的, 因此有全局解釋鎖(GIL), 一次只允許使用一個線程執行 python 位元組碼, 所以一個python進程不能同時使用多個 CPU 核心.
python程序員編寫代碼時無法控制 GIL, 然而, 在標準庫中所有執行阻塞型I/O操作的函數, 在登台操作系統返回結果時都會釋放GIL. 這意味著IO密集型python程序能從中受益.
使用concurrent.futures模塊啟動進程
一個python進程只有一個 GIL. 多個python進程就能繞開GIL, 因此這種方法就能利用所有的 CPU 核心. concurrent.futures 模塊就實現了真正的並行計算, 因為它使用 ProcessPoolExecutor 把工作交個多個python進程處理.
ProcessPoolExecutor 和 ThreadPoolExecutor 類都實現了通用的 Executor 介面, 因此使用 concurrent.futures 能很輕鬆把基於線程的方案轉成基於進程的方案.
Python
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))
改成:
Python
def download_many(cc_list):
with futures.ProcessPoolExecutor() as executor:
res = executor.map(download_one, sorted(cc_list))
ThreadPoolExecutor.__init__ 方法需要 max_workers 參數,指定線程池中線程的數量; 在 ProcessPoolExecutor 類中, 這個參數是可選的.
第十八章: 使用 asyncio 包處理並發
並發是指一次處理多件事。
並行是指一次做多件事。
二者不同,但是有聯繫。一個關於結構,一個關於執行。並發用於制定方案,用來解決可能(但未必)並行的問題。—— Rob Pike Go 語言的創造者之一並行是指兩個或者多個事件在同一時刻發生, 而並發是指兩個或多個事件在同一時間間隔發生. 真正運行並行需要多個核心, 現在筆記本一般有 4 個 CPU 核心, 但是通常就有超過 100 個進程同時運行. 因此, 實際上大多數進程都是並發處理的, 而不是並行處理. 計算機始終運行著 100 多個進程, 確保每個進程都有機會取得發展, 不過 CPU 本身同時做的事情不會超過四件.
本章介紹 asyncio 包, 這個包使用事件循環驅動的協程實現並發. 這個庫有龜叔親自操刀. asyncio 大量使用 yield from 表達式, 因此不兼容 python3.3 以下的版本.
線程與協程對比
一個藉由 threading 模塊使用線程, 一個藉由 asyncio 包使用協程實現來進行比對.
Python
import threading
import itertools
import time
def spin(msg, done): # 這個函數會在單獨的線程中運行
for char in itertools.cycle(|/-\): # 這其實是個無限循環,因為 itertools.cycle 函數會從指定的序列中反覆不斷地生成元素
status = char + + msg
print(status)
if done.wait(.1): # 如果進程被通知等待, 那就退出循環
break
def slow_function(): # 假設這是耗時的計算
# pretend waiting a long time for I/O
time.sleep(3) # 調用 sleep 函數會阻塞主線程,不過一定要這麼做,以便釋放 GIL,創建從屬線程
return 42
def supervisor(): # 這個函數設置從屬線程,顯示線程對象,運行耗時的計算,最後殺死線程。
done = threading.Event()
spinner = threading.Thread(target=spin,
args=(thinking!, done))
print(spinner object:, spinner) # 顯示從屬線程對象。輸出類似於 <Thread(Thread-1, initial)>
spinner.start() # 啟動從屬線程
result = slow_function() # 運行 slow_function 函數,阻塞主線程。同時,從屬線程以動畫形式顯示旋轉指針
done.set() # 改變 signal 的狀態;這會終止 spin 函數中的那個 for 循環
spinner.join() # 等待 spinner 線程結束
return result
if __name__ == __main__:
result = supervisor()
print(Answer:, result)
這是使用 threading 的案例, 讓子線程在 3 秒內不斷列印, 在python中, 沒有提供終止線程的API. 若想關閉線程, 必須給線程發送消息.
下面看看使用 @asyncio.coroutine 裝飾器替代協程, 實現相同的行為:
Python
import asyncio
import itertools
@asyncio.coroutine # 交給 asyncio 處理的協程要使用 @asyncio.coroutine 裝飾
def spin(msg):
for char in itertools.cycle(|/-\):
status = char + + msg
print(status)
try:
yield from asyncio.sleep(.1) # 使用 yield from asyncio.sleep(.1) 代替 time.sleep(.1),這樣的休眠不會阻塞事件循環。
except asyncio.CancelledError: # 如果 spin 函數蘇醒後拋出 asyncio.CancelledError 異常,其原因是發出了取消請求,因此退出循環。
break
@asyncio.coroutine
def slow_function(): # slow_function 函數是協程,在用休眠假裝進行 I/O 操作時,使用 yield from 繼續執行事件循環。
# pretend waiting a long time for I/O
yield from asyncio.sleep(3) # yield from asyncio.sleep(3) 表達式把控制權交給主循環,在休眠結束後恢復這個協程。
return 42
@asyncio.coroutine
def supervisor(): # supervisor 函數也是協程
spinner = asyncio.async(spin(thinking!)) # asyncio.async(...) 函數排定 spin 協程的運行時間,使用一個 Task 對象包裝spin 協程,並立即返回。
print(spinner object:, spinner)
result = yield from slow_function() # 驅動 slow_function() 函數。結束後,獲取返回值。
# 同時,事件循環繼續運行,因為slow_function 函數最後使用 yield from asyncio.sleep(3) 表達式把控制權交回給了主循環。
spinner.cancel() # Task 對象可以取消;取消後會在協程當前暫停的 yield 處拋出 asyncio.CancelledError 異常。協程可以捕獲這個異常,也可以延遲取消,甚至拒絕取消。
return result
if __name__ == __main__:
loop = asyncio.get_event_loop() # 獲取事件循環的引用
result = loop.run_until_complete(supervisor()) # 驅動 supervisor 協程,讓它運行完畢;這個協程的返回值是這次調用的返回值。
loop.close()
print(Answer:, result)
asyncio 包使用的協程是比較嚴格的定義, 適合 asyncio API 的協程在定義體中必須使用 yield from , 而不是使用 yield . 此外, asyncio 的協程要由調用方驅動, 例如 asyncio.async(…) , 從而驅動協程. 最後由 @asyncio.coroutine 裝飾器應用在協程上.
這兩種 supervisor 實現之間的主要區別概述如下:
- asyncio.Task 對象差不多與 threading.Thread 對象等效。「Task對象像是實現協作式多任務的庫(例如 gevent)中的綠色線程(green thread)」。
- Task 對象用於驅動協程,Thread 對象用於調用可調用的對象。
- Task 對象不由自己動手實例化,而是通過把協程傳給 asyncio.async(…) 函數或 loop.create_task(…) 方法獲取。
- 獲取的 Task 對象已經排定了運行時間(例如,由 asyncio.async 函數排定);Thread 實例則必須調用 start 方法,明確告知讓它運行。
- 在線程版 supervisor 函數中,slow_function 函數是普通的函數,直接由線程調用。在非同步版 supervisor 函數中,slow_function 函數是協程,由 yield from 驅動。
- 沒有 API 能從外部終止線程,因為線程隨時可能被中斷,導致系統處於無效狀態。如果想終止任務,可以使用 Task.cancel() 實例方法,在協程內部拋出 CancelledError 異常。協程可以在暫停的 yield 處捕獲這個異常,處理終止請求。
- supervisor 協程必須在 main 函數中由 loop.run_until_complete 方法執行。
多線程編程是比較困難的, 因為調度程序任何時候都能中斷線程, 必須記住保留鎖, 去保護程序中重要部分, 防止多線程在執行的過程中斷.
而協程默認會做好全方位保護, 以防止中斷. 我們必須顯示產出才能讓程序的餘下部分運行. 對協程來說, 無需保留鎖, 而在多個線程之間同步操作, 協程自身就會同步, 因為在任意時刻, 只有一個協程運行.
從期物、任務和協程中產出
在 asyncio 包中, 期物和協程關係緊密, 因為可以使用 yield from 從 asyncio.Future 對象中產出結果. 也就是說, 如果 foo 是協程函數, 或者是返回 Future 或 Task 實例的普通函數, 那麼可以用 res = yield from foo() .
為了執行這個操作, 必須排定協程的運行時間, 然後使用 asyncio.Task 對象包裝協程. 對協程來說, 獲取 Task 對象主要有兩種方式:
- asyncio.async(coro_or_future, *, loop=None) : 這個函數統一了協程和期物:第一個參數可以是二者中的任何一個。如果是 Future 或 Task 對象,那就原封不動地返回。如果是協程,那麼 async 函數會調用 loop.create_task(…) 方法創建 Task 對象。loop 關鍵字參數是可選的,用於傳入事件循環;如果沒有傳入,那麼 async 函數會通過調用 asyncio.get_event_loop() 函數獲取循環對象。
- BaseEventLoop.create_task(coro) : 這個方法排定協程的執行時間,返回一個 asyncio.Task 對象。如果在自定義的 BaseEventLoop 子類上調用,返回的對象可能是外部庫(如 Tornado)中與 Task 類兼容的某個類的實例。
asyncio 包中有多個函數會自動(使用 asyncio.async 函數) 把參數指定的協程包裝在 asyncio.Task 對象中.
使用asyncio和aiohttp包下載
asyncio 包只直接支持 TCP 和 UDP. 如果像使用 HTTP 或其他協議, 就需要藉助第三方包. 使用的幾乎都是 aiohttp 包. 以下載圖片為例:
Python
import asyncio
import aiohttp
from flags import BASE_URL, save_flag, show, main
@asyncio.coroutine
def get_flag(cc): # 協程應該使用 @asyncio.coroutine 裝飾。
url = {}/{cc}/{cc}.gif.format(BASE_URL, cc=cc.lower())
resp = yield from aiohttp.request(GET, url) # 阻塞的操作通過協程實現
image = yield from resp.read() # 讀取響應內容是一項單獨的非同步操作
return image
@asyncio.coroutine
def download_one(cc): # download_one 函數也必須是協程,因為用到了 yield from
image = yield from get_flag(cc)
show(cc)
save_flag(image, cc.lower() + .gif)
return cc
def download_many(cc_list):
loop = asyncio.get_event_loop() # 獲取事件循環底層實現的引用
to_do = [download_one(cc) for cc in sorted(cc_list)] # 調用 download_one 函數獲取各個國旗,然後構建一個生成器對象列表
wait_coro = asyncio.wait(to_do) # 雖然函數的名稱是 wait,但它不是阻塞型函數。wait 是一個協程,等傳給它的所有協程運行完畢後結束
res, _ = loop.run_until_complete(wait_coro) # 執行事件循環,直到 wait_coro 運行結束
loop.close() # 關閉事件循環
return len(res)
if __name__ == __main__:
main(download_many)
asyncio.wait(…) 協程參數是一個由期物或協程構成的可迭代對象, wait 會分別把各個協程裝進一個 Task 對象. 最終的結果是, wait 處理的所有對象都通過某種方式變成 Future 類的實例. wait 是協程函數, 因此返回的是一個協程或生成器對象. 為了驅動協程, 我們把協程傳給 loop.run_until_complete(…) 方法.
loop.run_until_complete 方法的參數是一個期物或協程. 如果是協程, run_until_complete 方法與 wait 函數一樣, 把協程包裝進一個 Task 對象中. 因為協程都是由 yield from 驅動, 這正是 run_until_complete 對 wait 返回返回的 wait_coro 對象所做的事. 運行結束後返回兩個元素, 第一個是是結束的期物, 第二個是未結束的期物.
避免阻塞型調用
有兩種方法能避免阻塞型調用中止整個應用程序的進程:
- 在單獨的線程中運行各個阻塞型操作
- 把每個阻塞型操作轉換成非阻塞的非同步調用使用
多線程是可以的, 但是會消耗比較大的內存. 為了降低內存的消耗, 通常使用回調來實現非同步調用. 這是一種底層概念, 類似所有並發機制中最古老最原始的那種–硬體中斷. 使用回調時, 我們不等待響應, 而是註冊一個函數, 在發生某件事時調用. 這樣, 所有的調用都是非阻塞的.
非同步應用程序底層的事件循環能依靠基礎設置的中斷, 線程, 輪詢和後台進程等待等, 確保多個並發請求能取得進展並最終完成, 這樣才能使用回調. 事件循環獲得響應後, 會回過頭來調用我們指定的回調. 如果做法正確, 事件循環和應用代碼公共的主線程絕不會阻塞.
把生成器當做協程使用是非同步編程的另一種方式. 對事件循環來說, 調用回調與在暫停的協程上調用 .send() 效果差不多.
使用Executor對象,防止阻塞事件循環
訪問本地文件會阻塞, 而CPython底層在阻塞型I/O調用時會釋放 GIL, 因此另一個線程可以繼續.
因為 asyncio 事件不是通過多線程來完成, 因此 save_flag 用來保存圖片的函數阻塞了與 asyncio 事件循環共用的唯一線程, 因此保存文件時, 真箇應用程序都會凍結. 這個問題的解決辦法是, 使用事件循環對象的 run_in_executor 方法.
asyncio 的事件循環背後維護者一個 ThreadPoolExecutor 對象, 我們可以調用 run_in_executor 方法, 把可調用的對象發給它執行:
Python
@asyncio.coroutine
def download_one(cc, base_url, semaphore, verbose):
try:
with (yield from semaphore):
image = yield from get_flag(base_url, cc)
except web.HTTPNotFound:
status = HTTPStatus.not_found
msg = not found
except Exception as exc:
raise FetchError(cc) from exc
else:
loop = asyncio.get_event_loop() # 獲取事件循環對象的引用
loop.run_in_executor(None, # run_in_executor 方法的第一個參數是 Executor 實例;如果設為 None,使用事件循環的默認 ThreadPoolExecutor 實例。
save_flag, image, cc.lower() + .gif) # 餘下的參數是可調用的對象,以及可調用對象的位置參數
status = HTTPStatus.ok
msg = OK
if verbose and msg:
print(cc, msg)
return Result(status, cc)
第十九章: 動態屬性和特性
在python中, 數據的屬性和處理數據的方法都可以稱為 屬性 . 除了屬性, pythpn 還提供了豐富的 API, 用於控制屬性的訪問許可權, 以及實現動態屬性, 如 obj.attr 方式和 __getattr__ 計算屬性.
動態創建屬性是一種元編程,
使用動態屬性轉換數據
通常, 解析後的 json 數據需要形如 feed[『Schedule』][『events』][40][『name』] 形式訪問, 必要情況下我們可以將它換成以屬性訪問方式 feed.Schedule.events[40].name獲得那個值.
Python
from collections import abc
class FrozenJSON:
"""一個只讀介面,使用屬性表示法訪問JSON類對象
"""
def __init__(self, mapping):
self.__data = dict(mapping)
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON.build(self.__data[name]) # 從 self.__data 中獲取 name 鍵對應的元素
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else: # 如果既不是字典也不是列表,那麼原封不動地返回元素
return obj
使用 new 方法以靈活的方式創建對象
我們通常把 __init__ 成為構造方法, 這是從其他語言借鑒過來的術語. 其實, 用於構造實例的特殊方法是 __new__ : 這是個類方法, 必須返回一個實例. 返回的實例將作為以後的 self 傳給 __init__ 方法.
第二十章: 屬性描述符
描述符是實現了特性協議的類, 這個協議包括 __get__, __set__ 和 __delete__ 方法. 通常, 可以實現部分協議.
覆蓋型與非覆蓋型描述符對比
python存取屬性的方式是不對等的. 通過實例讀取屬性時, 通常返回的是實例中定義的屬性, 但是, 如果實例中沒有指定的屬性, 那麼會從獲取類屬性. 而實例中屬性賦值時, 通常會在實例中創建屬性, 根本不影響類.
這種不對等的處理方式對描述符也有影響. 根據是否定義 __set__ 方法, 描述符可分為兩大類: 覆蓋型描述符和與非覆蓋型描述符.
實現 __set__ 方法的描述符屬於覆蓋型描述符, 因為雖然描述符是類屬性, 但是實現 __set__方法的話, 會覆蓋對實例屬性的賦值操作. 因此作為類方法的 __set__ 需要傳入一個實例 instance . 看個例子:
Python
def print_args(*args): # 列印功能
print(args)
class Overriding: # 設置了 __set__ 和 __get__
def __get__(self, instance, owner):
print_args(get, self, instance, owner)
def __set__(self, instance, value):
print_args(set, self, instance, value)
class OverridingNoGet: # 沒有 __get__ 方法的覆蓋型描述符
def __set__(self, instance, value):
print_args(set, self, instance, value)
class NonOverriding: # 沒有 __set__ 方法,所以這是非覆蓋型描述符
def __get__(self, instance, owner):
print_args(get, self, instance, owner)
class Managed: # 託管類,使用各個描述符類的一個實例
over = Overriding()
over_no_get = OverridingNoGet()
non_over = NonOverriding()
def spam(self):
print(-> Managed.spam({}).format(repr(self)))
覆蓋型描述符
Python
obj = Managed()
obj.over # (get, <__main__.Overriding object>, <__main__.Managed object>, <class __main__.Managed>)
obj.over = 7 # (set, <__main__.Overriding object>, <__main__.Managed object>, 7)
obj.over # (get, <__main__.Overriding object>, <__main__.Managed object>, <class __main__.Managed>)
名為 over 的實例屬性, 會覆蓋讀取和賦值 obj.over 的行為.
沒有 __get__ 方法的覆蓋型描述符
Python
obj = Managed()
obj.over_no_get
obj.over_no_get = 7 # (set, <__main__.OverridingNoGet object>, <__main__.Managed object>, 7)
obj.over_no_get
只有在賦值操作的時候才回覆蓋行為.
方法是描述符
python的類中定義的函數屬於綁定方法, 如果用戶定義的函數都有 __get__ 方法, 所以依附到類上, 就相當於描述符.
obj.spam 和 Managed.spam 獲取的是不同的對象. 前者是 後者是 .
函數都是非覆蓋型描述符. 在函數上調用 __get__ 方法時傳入實例作為 self , 得到的是綁定到那個實例的方法. 調用函數的 __get__ 時傳入的 instance 是 None , 那麼得到的是函數本身. 這就是形參 self 的隱式綁定方式.
描述符用法建議
使用特性以保持簡單
內置的 property 類創建的是覆蓋型描述符, __set__ 和 __get__ 都實現了.只讀描述符必須有 set 方法
如果要實現只讀屬性, __get__ 和 __set__ 兩個方法必須都定義, 柔則, 實例的同名屬性會覆蓋描述符.用於驗證的描述符可以只有 set 方法
什麼是用於驗證的描述符, 比方有個年齡屬性, 但它只能被設置為數字, 這時候就可以只定義 __set__ 來驗證值是否合法. 這種情況不需要設置 __get__ , 因為實例屬性直接從 __dict__ 中獲取, 而不用去觸發 __get__ 方法.第二十一章: 類元編程
類元編程是指在運行時創建或定製類的技藝. 在python中, 類是一等對象, 因此任何時候都可以使用函數創建類, 而無需使用 class 關鍵字. 類裝飾器也是函數, 不過能夠審查, 修改, 甚至把被裝飾的類替換成其他類.
元類是類元編程最高級的工具. 什麼是元類呢? 比如說 str 是創建字元串的類, int 是創建整數的類. 那麼元類就是創建類的類. 所有的類都由元類創建. 其他 class 只是原來的」實例」.
本章討論如何在運行時創建類.
類工廠函數
標準庫中就有一個例子是類工廠函數–具名元組( collections.namedtuple ). 我們把一個類名和幾個屬性傳給這個函數, 它會創建一個 tuple 的子類, 其中的元素通過名稱獲取.
假設我們創建一個 record_factory , 與具名元組具有相似的功能:
Python
>>> Dog = record_factory(Dog, name weight owner)
>>> rex = Dog(Rex, 30, Bob)
>>> rex
Dog(name=Rex, weight=30, owner=Bob)
>>> rex.weight = 32
>>> Dog.__mro__
(<class factories.Dog>, <class object>)
我們要做一個在運行時創建類的, 類工廠函數:
Python
def record_factory(cls_name, field_names):
try:
field_names = field_names.replace(,, ).split() # 屬性拆分
except AttributeError: # no .replace or .split
pass # assume its already a sequence of identifiers
field_names = tuple(field_names) # 使用屬性名構建元組,這將成為新建類的 __slots__ 屬性
def __init__(self, *args, **kwargs): # 這個函數將成為新建類的 __init__ 方法
attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self): # 實現 __iter__ 函數, 變成可迭代對象
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self): # 生成友好的字元串表示形式
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__)
return type(cls_name, (object,), cls_attrs) # 調用元類 type 構造方法,構建新類,然後將其返回
type 就是元類, 實例的最後一行會構造一個類, 類名是 cls_name, 唯一直接的超類是 object.
在python中做元編程時, 最好不要用 exec 和 eval 函數. 這兩個函數會帶來嚴重的安全風險.
元類基礎知識
元類是製造類的工廠, 不過不是函數, 本身也是類. 元類是用於構建類的類.
為了避免無限回溯, type 是其自身的實例. object 類和 type 類關係很獨特, object 是 type的實例, 而 type 是 object 的子類.
元類的特殊方法 prepare
type 構造方法以及元類的 __new__ 和 __init__ 方法都會收到要計算的類的定義體, 形式是名稱到屬性的映像. 在默認情況下, 這個映射是字典, 屬性在類的定義體中順序會丟失. 這個問題的解決辦法是, 使用python3引入的特殊方法 __prepare__ , 這個方法只在元類中有用, 而且必須聲明為類方法(即要使用 @classmethod 裝飾器定義). 解釋器調用元類的 __new__ 方法之前會先調用 __prepare__ 方法, 使用類定義體中的屬性創建映射.
__prepare__ 的第一個參數是元類, 隨後兩個參數分別是要構建類的名稱和基類組成的原則, 返回值必須是映射.
Python
class EntityMeta(type):
"""Metaclass for business entities with validated fields"""
@classmethod
def __prepare__(cls, name, bases):
return collections.OrderedDict() # 返回一個空的 OrderedDict 實例,類屬性將存儲在裡面。
def __init__(cls, name, bases, attr_dict):
super().__init__(name, bases, attr_dict)
cls._field_names = [] # 中創建一個 _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):
"""Business entity with validated fields"""
@classmethod
def field_names(cls): # field_names 類方法的作用簡單:按照添加欄位的順序產出欄位的名稱
for name in cls._field_names:
yield name
結語
python是一門即容易上手又強大的語言.
作者: weapon
原文鏈接:《流暢的python》閱讀筆記 - 編程進階之路 - SegmentFault
聲明:文章著作權歸作者所有,如有侵權請聯繫小編刪除
推薦閱讀:
TAG:Python教程 |