Effective Python --編寫(xiě)高質(zhì)量Python代碼的59個(gè)有效方法 (讀書(shū)筆記 每日更新一條 Updated 2019.1.28)
好記性不如爛筆頭,讀讀寫(xiě)寫(xiě)才能記得牢固些
第 1 章 用 Pythonic 方式來(lái)思考 1
第 1 條:確認(rèn)自己所用的 Python 版本 1
2to3和six等工具可以吧Python2代碼適配到Python3版本上。
- 有兩個(gè)版本的 Python 處于活躍狀態(tài),它們是: Python 2 與 Python 3。
- 有很多種流行的 Python 運(yùn)行時(shí)環(huán)境,例如, CPython、 Jython、 IronPython 以及
PyPy 等。 - 在操作系統(tǒng)的命令行中運(yùn)行 Python 時(shí),請(qǐng)確保該 Python 的版本與你想使用的
Python 版本相符。 - 由于 Python 社區(qū)把開(kāi)發(fā)重點(diǎn)放在 Python 3 上,所以在開(kāi)發(fā)后續(xù)項(xiàng)目時(shí),應(yīng)該優(yōu)先考慮采用 Python 3。
第 2 條:遵循 PEP 8 風(fēng)格指南 3
-
空白:
- 不要使用 tab 縮進(jìn),使用空格來(lái)縮進(jìn)
- 使用四個(gè)空格縮進(jìn),使用四個(gè)空格對(duì)長(zhǎng)表達(dá)式換行縮進(jìn)
- 每行的字符數(shù)不應(yīng)該超過(guò) 79
- class和funciton之間用兩個(gè)空行,class的method之間用一個(gè)空行
- list索引和函數(shù)調(diào)用,關(guān)鍵字參數(shù)賦值不要在兩旁加空格
- 變量賦值前后都用一個(gè)空格
-
命名
- 函數(shù),變量以及屬性應(yīng)該使用小寫(xiě),如果有多個(gè)單詞推薦使用下劃線(xiàn)進(jìn)行連接,如lowercase_underscore
- 被保護(hù) 的屬性應(yīng)該使用 單個(gè) 前導(dǎo)下劃線(xiàn)來(lái)聲明。
- 私有 的屬性應(yīng)該使用 兩個(gè) 前導(dǎo)下劃線(xiàn)來(lái)進(jìn)行聲明。
- 類(lèi)以及異常信息 應(yīng)該使用單詞 首字母大寫(xiě) 形式,也就是我們經(jīng)常使用的駝峰命名法,如CapitalizedWord。
- 模塊級(jí) 別的常量應(yīng)該使用 全部大寫(xiě) 的形式, 如ALL_CAPS。
- 類(lèi)內(nèi)部的實(shí)例方法的應(yīng)該將self作為其第一個(gè)參數(shù)。且self也是對(duì)當(dāng)前類(lèi)對(duì)象的引用。
- 類(lèi)方法應(yīng)該使用cls來(lái)作為其第一個(gè)參數(shù)。且self引用自當(dāng)前類(lèi)。
-
表達(dá)式和語(yǔ)句( Python之禪: 每件事都應(yīng)該有直白的做法,而且最好只有一種 )
- 使用內(nèi)聯(lián)否定(如 if a is not b) 而不是顯示的表達(dá)式(如if not a is b)。
- 不要簡(jiǎn)單地通過(guò)變量的長(zhǎng)度(if len(somelist) == 0)來(lái)判斷空值。使用隱式的方式如來(lái)假設(shè)空值的情況(如if not somelist 與 False來(lái)進(jìn)行比較)。
- 上面的第二條也適用于非空值(如[1],或者’hi’)。對(duì)這些非空值而言 if somelist默認(rèn)包含隱式的True。
- 避免將if , for, while, except等包含多個(gè)語(yǔ)塊的表達(dá)式寫(xiě)在一行內(nèi),應(yīng)該分割成多行。
- 總是把import語(yǔ)句寫(xiě)在Python文件的頂部。
- 當(dāng)引用一個(gè)模塊的時(shí)候使用絕對(duì)的模塊名稱(chēng),而不是與當(dāng)前模塊路徑相關(guān)的名稱(chēng)。例如要想引入bar包下面的foo模塊,應(yīng)該使用from bar import foo而不是import foo。
- 如果非要相對(duì)的引用,應(yīng)該使用明確的語(yǔ)法from . import foo。
- 按照以下規(guī)則引入模塊:標(biāo)準(zhǔn)庫(kù),第三方庫(kù),你自己的庫(kù)。每一個(gè)部分內(nèi)部也應(yīng)該按照字母順序來(lái)引入。
第 3 條:了解 bytes、 str 與 unicode 的區(qū)別 5
- Python3 兩種字符串類(lèi)型:bytes和str,bytes表示8-bit的二進(jìn)制值,str表示unicode字符。開(kāi)發(fā)者不能用 > 或 + 混同操作bytes和str實(shí)例
- Python2 兩種字符串類(lèi)型:str和unicode,str表示8-bit的二進(jìn)制值,unicode表示unicode字符。如果str只包含7位ASCII字符,那么通過(guò)相關(guān)的操作符才同時(shí)使用str與unicode
- 在對(duì)輸入的數(shù)據(jù)進(jìn)行操作之前,使用輔助函數(shù)來(lái)保證字符的序列
- 從文件中讀取或者寫(xiě)入二進(jìn)制數(shù)據(jù)時(shí),總應(yīng)該使用 ‘rb’ 或 ‘wb’ 等二進(jìn)制模式來(lái)開(kāi)啟文件
第 4 條:用輔助函數(shù)來(lái)取代復(fù)雜的表達(dá)式 8
- 開(kāi)發(fā)者很容易過(guò)度使用Python的語(yǔ)法特效,從而寫(xiě)出那種特別復(fù)雜并且難以理解的單行表達(dá)式
- 請(qǐng)把復(fù)雜的表達(dá)式移入輔助函數(shù)中,如果要反復(fù)使用相同的邏輯,那就更應(yīng)該這么做
- 使用 if/else 表達(dá)式,要比使用 or 或者 and 這樣的 Booolean 操作符更加清晰
第 5 條:了解切割序列的辦法 10
- 分片機(jī)制自動(dòng)處理越界問(wèn)題,但是最好在表達(dá)邊界大小范圍是更加的清晰。(如a[:20] 或者a[-20:])
- list,str,bytes和實(shí)現(xiàn)
__getitem__和__setitem__這兩個(gè)特殊方法的類(lèi)都支持slice操作 - 基本形式是:somelist[start:end],不包括end,可以使用負(fù)數(shù),-1 表示最后一個(gè),默認(rèn)正向選取,下標(biāo)0可以省略,最后一個(gè)下標(biāo)也可以省略
- slice list是shadow copy,somelist[0:]會(huì)復(fù)制原list,切割之后對(duì)新得到的列表進(jìn)行修改不會(huì)影響原來(lái)的列表
- slice賦值會(huì)修改slice list,即使長(zhǎng)度不一致(增刪改)
第 6 條:在單次切片操作內(nèi),不要同時(shí)指定 start、 end 和 stride 13
- 既有start和end, 又有stride的切割操作,會(huì)讓人感到困惑。
- 盡量使用stride為正數(shù),且不帶start或者end索引的切割操作。盡可能的避免在分片中使用負(fù)數(shù)值。
- 避免在分片中同時(shí)使用start,end,stride;如果非要使用,考慮兩次賦值(一個(gè)分片,一個(gè)調(diào)幅),或者使用內(nèi)置模塊itertoolsde 的 islice方法來(lái)進(jìn)行處理。
第 7 條:用列表推導(dǎo)來(lái)取代 map 和 filter 15
a = [1,2,3,4,5,6,7,8,9,10]
squares = [x*x for x in a]
squares = map(lambda x: x **2 ,a)
even_squares = [x**2 for x in a if x%2==0]
alt = map(lambda x: x**2, filter(lambda x: x%2==0,a))
assert even_squares== list(alt)
chile_rank = {'ghost':1,'habanero':2,'cayenne':3}
rank_dict = {rank:name for name,rank in child_rank.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
- 列表表達(dá)式比內(nèi)置的
map,filter更加清晰,因?yàn)?code>map,filter需要額外的lambda表達(dá)式的支持。 - 列表表達(dá)式允許你很容易的跳過(guò)某些輸入值,而一個(gè)
map沒(méi)有filter幫助的話(huà)就不能完成這一個(gè)功能。 - 字典和集合也都支持列表表達(dá)式
第 8 條:不要使用含有兩個(gè)以上表達(dá)式的列表推導(dǎo) 16
matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]]
squared = [[ x**2 for x in row] for row in matrix]
flat = [x for row in matrix for x in row]
my_lists = [
[[1, 2, 3],[4, 5, 6]],
# ...
]
# not good
flat = [ x for sublist in my_lists
for sublist2 in sublist
for x in sublist2]
# prefer
flat = []
for sublist in my_lists:
for sublist2 in sublist:
flat.append(sublist2)
- 列表表達(dá)式支持多層的循環(huán)和條件語(yǔ)句,以及每層循環(huán)內(nèi)部的條件語(yǔ)句。
- 當(dāng)列表表達(dá)式內(nèi)部多余兩個(gè)表達(dá)式的時(shí)候就會(huì)變得難于閱讀,這種寫(xiě)法應(yīng)該避免使用。
第 9 條:用生成器表達(dá)式來(lái)改寫(xiě)數(shù)據(jù)量較大的列表推導(dǎo) 18
列表生成式的缺點(diǎn)是,在推倒過(guò)程中,對(duì)輸入列表中的每一個(gè)值,可能都要?jiǎng)?chuàng)建只包含一個(gè)元素的新列表。這對(duì)于小的輸入序列可能是很好用的,但是大的輸入序列而言就很有可能導(dǎo)致你的程序崩潰。
ython提供了一個(gè)generator expression(生成器表達(dá)式),在程序運(yùn)行的過(guò)程中,生成其表達(dá)式不實(shí)現(xiàn)整個(gè)輸出序列,相反,生成其表達(dá)式僅僅是對(duì)從表達(dá)式中產(chǎn)生一個(gè)項(xiàng)目的迭代器進(jìn)行計(jì)算,說(shuō)白了就是每次僅僅處理一個(gè)迭代項(xiàng),而不是整個(gè)序列。
生成器表達(dá)式通過(guò)使用類(lèi)似于列表表達(dá)式的語(yǔ)法(在()之間而不是[]之間,僅此區(qū)別)來(lái)創(chuàng)建。
it = ( len(x) for x in open('/tmp/my_file.txt'))
print(next(it))
roots = ((x,x**0.5) for x in it)
print(next(roots))
- 當(dāng)遇到大輸入事件的時(shí)候,使用列表表達(dá)式可能導(dǎo)致一些問(wèn)題。
- 生成器表達(dá)式通過(guò)迭代的方式來(lái)處理每一個(gè)列表項(xiàng),可以防止出現(xiàn)內(nèi)存危機(jī)。
- 當(dāng)生成器表達(dá)式 處于鏈?zhǔn)綘顟B(tài)時(shí),會(huì)執(zhí)行的很迅速。
第 10 條:盡量用 enumerate 取代 range 20
# good
for i, flavor in enumerate(flavor_list):
print(‘%d: %s’ % (i + 1, flavor))
# not good
for i in range(len(flavor_list)):
flavor = flavor_list[i]
print(‘%d: %s’ % (i + 1, flavor))
-
enumerate提供了簡(jiǎn)潔的語(yǔ)法,再循環(huán)迭代一個(gè)迭代器的同時(shí)既能獲取下標(biāo),也能獲取當(dāng)前值。 - 可以添加第二個(gè)參數(shù)來(lái)指定索引開(kāi)始的序號(hào),默認(rèn)為
0
第 11 條:用 zip 函數(shù)同時(shí)遍歷兩個(gè)迭代器 21
names = ['Cecilia','Lise','Marie']
letters = [len(n) for n in names]
for name, count in zip(names, letters):
if count > max_letters:
longest_name = name
max_letters = count
- 內(nèi)置的zip函數(shù)可以并行的對(duì)多個(gè)迭代器進(jìn)行處理。
- 在Python3中,zip 相當(dāng)于生成器,會(huì)在遍歷過(guò)程中逐次產(chǎn)生元組。而在Python2中,zip返回的是一個(gè)包含了其處理好的所有元祖的一個(gè)集合。
- 如果所處理的迭代器的長(zhǎng)度不一致時(shí),zip會(huì)默認(rèn)截?cái)噍敵?,使得長(zhǎng)度為最先到達(dá)尾部的那個(gè)長(zhǎng)度。
- 內(nèi)置模塊itertools中的zip_longest函數(shù)可以并行地處理多個(gè)迭代器,而可以無(wú)視長(zhǎng)度不一致的問(wèn)題。
第 12 條:不要在 for 和 while 循環(huán)后面寫(xiě) else 塊 23
for i in range(2):
print('Loop %d' % i)
else:
print('Else block')
>>>
Loop 0
Loop 1
Else block
-
Python有用特殊的語(yǔ)法能夠讓else語(yǔ)塊在循環(huán)體結(jié)束的時(shí)候立刻得到執(zhí)行。 - 循環(huán)體后的
else語(yǔ)塊只有在循環(huán)體沒(méi)有觸發(fā)break語(yǔ)句的時(shí)候才會(huì)執(zhí)行。 - 避免在循環(huán)體的后面使用
else語(yǔ)塊,因?yàn)檫@樣的表達(dá)不直觀,而且容易誤導(dǎo)讀者。
第 13 條:合理利用 try/except/else/f inally 結(jié)構(gòu)中的每個(gè)代碼塊 25
UNDEFINED = object()
def divide_json(path):
handle = open(path, 'r+') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
op = json.loads(data) # May raise ValueError
value = (op['numerator'] / op['denominator']) # May raise ZeroDivisionError
except ZeroDivisionError as e:
return UNDEFINED
else:
op[‘result’] = value
result = json.dumps(op)
handle.seek(0)
handle.write(result) # May raise IOError
return value
finally:
handle.close() # Always runs
- try/finally組合語(yǔ)句可以使得你的代碼變得很整潔而無(wú)視try塊中是否發(fā)生異常。
- else塊可以最大限度的減少try塊中的代碼的長(zhǎng)度,并且可以可視化地辨別try/except成功運(yùn)行的部分。
- else塊經(jīng)常會(huì)被用于在try塊成功運(yùn)行后添加額外的行為,但是要確保代碼會(huì)在finally塊之前得到運(yùn)行。
finally 塊: 總是會(huì)執(zhí)行,可以用來(lái)關(guān)閉文件句柄之類(lèi)的
else 塊 : try 塊沒(méi)有發(fā)生異常則執(zhí)行 else 塊,有了 else 塊,我們可以盡量減少 try 塊的代碼量
第 2 章 函數(shù) 28
第 14 條:盡量用異常來(lái)表示特殊情況,而不要返回 None 28
- 返回
None的函數(shù)來(lái)作為特殊的含義是容易出錯(cuò)的,因?yàn)?code>None和其他的變量(例如zero,空字符串)在條件表達(dá)式的判斷情景下是等價(jià)的。 - 通過(guò)觸發(fā)一個(gè)異常而不是直接的返回
None是比較常用的一個(gè)方法。這樣調(diào)用方就能夠合理地按照函數(shù)中的說(shuō)明文檔來(lái)處理由此而引發(fā)的異常了。
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invalid inputs') from e
# use like this:
x, y = 5, 2
try:
result = divide(x, y)
except ValueError:
print("Invalid inputs")
else:
print("Result is %.1f"% result)
第 15 條:了解如何在閉包里使用外圍作用域中的變量 30
Python支持閉包。閉包是一種定義在某個(gè)作用域中的函數(shù),這種函數(shù)引用了作用域中的變量。
Python的函數(shù)是一級(jí)對(duì)象,我們可以直接引用函數(shù),把函數(shù)賦值給變量、當(dāng)成參數(shù)傳遞給其它函數(shù)等等。。
Python編譯器變量查找域的順序:
- 當(dāng)前函數(shù)的作用域
- 任何其他的封閉域(比如其他的包含著的函數(shù))。
- 包含該段代碼的模塊域(也稱(chēng)之為全局域)
- 內(nèi)置域(包含了像len,str等函數(shù)的域)
# 優(yōu)先排序
def sort_priority2(values, group):
found = False # 作用域:sort_priority2
def helper(x):
if x in group:
found = True # 作用域: helper
return (0, x)
return (1, x) # found在helper的作用域就會(huì)由helper轉(zhuǎn)至sort_priority2函數(shù)
values.sort(key=helper)
return found
-
獲取閉包中的數(shù)據(jù)-Python3
def srt_priority3(numbers, group): found = False def helper(x): nonlocal found # 表明found是閉包外數(shù)據(jù) if x in group: found = True return (0, x) return (1, x) numbers.sort(key=helper) return found當(dāng)數(shù)據(jù)在閉包外將被賦值到另一個(gè)域時(shí),nonlocal 語(yǔ)句使得這個(gè)過(guò)程變得很清晰。它也是對(duì)global語(yǔ)句的一個(gè)補(bǔ)充,可以明確的表明變量的賦值應(yīng)該被直接放置到模塊域中。
然而,像這樣的反模式,對(duì)使用在那些簡(jiǎn)單函數(shù)之外的其他的任何地方。nonlocal引起的副作用是難以追蹤的,而在那些包含著nonlocal語(yǔ)句和賦值語(yǔ)句交叉聯(lián)系的大段代碼的函數(shù)的內(nèi)部則尤為明顯。
當(dāng)你感覺(jué)自己的nonlocal語(yǔ)句開(kāi)始變的復(fù)雜的時(shí)候,我非常建議你重構(gòu)一下代碼,寫(xiě)成一個(gè)工具類(lèi)。這里,我定義了一個(gè)實(shí)現(xiàn)了與上面的那個(gè)函數(shù)功能相一致的工具類(lèi)。雖然有點(diǎn)長(zhǎng),但是代碼卻變得更加的清晰了(詳見(jiàn)第23項(xiàng):對(duì)于簡(jiǎn)單接口使用函數(shù)而不是類(lèi)里面的
__call__方法)class Sorter(object): def __init__(self, group): self.group = group self.found = False def __call__(self, x): if x in self.group: self.found = True return (0, x) return (1, x) sorter = Sorter(group) numbers.sort(key=sorter) assert sorter is True -
Python2中的作用域
Python2是不支持nonlocal關(guān)鍵字的。為了實(shí)現(xiàn)相似的功能,你需要廣泛的借助于Python的作用與域規(guī)則。雖然這個(gè)方法并不是完美的,但是這是Python中比較常用的一種做法。# Python2 def sort_priority(numbers, group): found = [False] def helper(x): if x in group: found[0] = True return (0, x) return (1, x) numbers.sort(sort=helper) return found[0]就像上面解釋的那樣,Python 將會(huì)橫向查找該變量所在的域來(lái)分析其當(dāng)前值。技巧就是發(fā)現(xiàn)的值是一個(gè)易變的列表。這意味著一旦檢索,閉包就可以修改found的狀態(tài)值,并且把內(nèi)部數(shù)據(jù)的改變發(fā)送到外部,這也就打破了閉包引發(fā)的局部變量作用域無(wú)法被改變的難題。其根本還是在于列表本身元素值可以被改變,這才是此函數(shù)可以正常工作的關(guān)鍵。
當(dāng)found為一個(gè)dictionary類(lèi)型的時(shí)候,也是可以正常工作的,原理與上文所言一致。此外,found還可以是一個(gè)集合,一個(gè)你自定義的類(lèi)等等。
要點(diǎn):
- 閉包函數(shù)可以從變量被定義的作用域內(nèi)引用變量。
- 默認(rèn)地,閉包不能通過(guò)賦值來(lái)影響其檢索域。
- 在Python3中,可以使用nonlocal關(guān)鍵字來(lái)突破閉包的限制,進(jìn)而在其檢索域內(nèi)改變其值。(global 關(guān)鍵字用于使用全局變量,nonlocal 關(guān)鍵字用于使用局部變量(函數(shù)內(nèi)))
- Python2中沒(méi)有nonlocal關(guān)鍵字,替代方案就是使用一個(gè)單元素(如列表,字典,集合等等)來(lái)實(shí)現(xiàn)與nonlocal一致的功能。
- 除了簡(jiǎn)單的函數(shù),在其他任何地方都應(yīng)該盡力的避免使用nonlocal關(guān)鍵字。
第 16 條:考慮用生成器來(lái)改寫(xiě)直接返回列表的函數(shù) 35
- 相較于返回一個(gè)列表的情況,替代方案中使用生成器可以使得代碼變得更加的清晰。
- 生成器返回的迭代器,是在其生成器內(nèi)部一個(gè)把值傳遞給了
yield變量的集合。 - 生成器可以處理很大的輸出序列就是因?yàn)樗谔幚淼臅r(shí)候不會(huì)完全的包含所有的數(shù)據(jù)。
#list
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index + 1)
return result
address = 'Four score and seven years ago...'
result = index_words(address)
print(result[:3]) # [0, 5, 11]
#generator
def index_words_iter(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
result = list(index_words_iter(address))
第 17 條:在參數(shù)上面迭代時(shí),要多加小心 37
- 多次遍歷輸入?yún)?shù)的時(shí)候應(yīng)該多加小心。如果參數(shù)是迭代器的話(huà)你可能看到奇怪的現(xiàn)象或者缺少值現(xiàn)象的發(fā)生。
- Python的iterator協(xié)議定義了容器和迭代器在iter和next下對(duì)于循環(huán)和相關(guān)表達(dá)式的關(guān)系。
只要實(shí)現(xiàn)了__iter__方法,你就可以很容易的定義一個(gè)可迭代的容器類(lèi)。 - 通過(guò)連續(xù)調(diào)用兩次iter方法,你就可以預(yù)先檢測(cè)一個(gè)值是不是迭代器而不是容器。兩次結(jié)果一致那就是迭代器,否則就是容器了。調(diào)用內(nèi)置的next函數(shù),可以令迭代器前進(jìn)一步。
#generator不能重用的例子
def read_visits(data_path):
with open(data_path,'r') as f:
for line in f:
yield int(line)
it = read_visits('tmp/my_numbers.txt')
print(list(it))
print(list(it)) # 這里其實(shí)已經(jīng)執(zhí)行到頭了
>>>
[15, 35, 80]
[]
# 如何解決?每次調(diào)用都創(chuàng)建iterator避免上面list分配內(nèi)存
def normalize_func(get_iter): # get_iter 是函數(shù)
total = sum(get_iter()) # New iterator
result = []
for value in get_iter(): # New iterator
percent = 100 * value / total
result.append(percent)
return result
percentages = normalize_func(lambda: read_visits(path))
for循環(huán)會(huì)調(diào)用內(nèi)置iter函數(shù),進(jìn)而調(diào)用對(duì)象的__iter__方法,__iter__會(huì)返回iterator對(duì)象(實(shí)現(xiàn)__next__方法)。用iter函數(shù)檢測(cè)iterator:
class ReadVists(object):
def __int__(self, data_path):
self.data_path = data_path
# 在自己的類(lèi)中把__iter__實(shí)現(xiàn)為生成器,就可以實(shí)現(xiàn)一個(gè)可以迭代的容器類(lèi)
def __iter__(self):
with open(self.data_path) as f:
for line in f:
yield int(line)
def normalize_defensive(numbers):
if iter(numbers) is iter(numbers): # 是個(gè)迭代器
raise TypeError('Must supply a container')
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
visits = [15, 35, 80]
normalize_defensive(visits) # no error
visits = ReadVIsitors(path)
normalize_defensive(visits) # no error
# 但是如果輸入值不是一個(gè)容器類(lèi)的話(huà),就會(huì)引發(fā)異常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
第 18 條:用數(shù)量可變的位置參數(shù)減少視覺(jué)雜訊 41
- 通過(guò)使用
*args定義語(yǔ)句,函數(shù)可以接收可變數(shù)量的位置參數(shù)。 - 你可以通過(guò)
*操作符來(lái)將序列中的元素作為位置變量。 - 帶有
*操作符的生成器變量可能會(huì)引起程序的內(nèi)存溢出,或者機(jī)器宕機(jī)。 - 為可以接受
*args的函數(shù)添加新的位置參數(shù)可以產(chǎn)生難于發(fā)現(xiàn)的問(wèn)題,應(yīng)該謹(jǐn)慎使用。
def log(message, *values):
if not values:
print(message)
else:
values_str = ', '.join(str(x) for x in values)
print('%s: %s' % (message, values_str))
log('My numbers are', 1, 2)
log('Hi there')
第 19 條:用關(guān)鍵字參數(shù)來(lái)表達(dá)可選的行為 43
- 函數(shù)的參數(shù)值即可以通過(guò)位置被指定,也可以通過(guò)關(guān)鍵字來(lái)指定。
- 相較于使用位置參數(shù)賦值,使用關(guān)鍵字來(lái)賦值會(huì)讓你的賦值語(yǔ)句邏輯變得更加的清晰。
- 帶有默認(rèn)參數(shù)的關(guān)鍵字參數(shù)函數(shù)可以很容易的添加新的行為,尤其適合向后兼容。
- 可選的關(guān)鍵字參數(shù)應(yīng)該優(yōu)于位置參數(shù)被考慮使用。
關(guān)鍵字參數(shù)的好處:
- 代碼可讀性的提高
- 以在定義的時(shí)候初始化一個(gè)默認(rèn)值
- 在前面的調(diào)用方式不變的情況下可以很好的拓展函數(shù)的參數(shù),不用修改太多的代碼
# before
def flow_rate(weight_diff, time_diff, period=1):
return (weight_diff / time_diff) * period
# after
def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
return ((weight_diff / units_per_kg) / time_diff) * period
flow_per_second = flow_rate(weight_diff, time_diff)
flow_per_hour = flow_rate(weight_diff, time_diff, period=3600)
pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)
pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2) # 不推薦
第 20 條:用 None 和文檔字符串來(lái)描述具有動(dòng)態(tài)默認(rèn)值的參數(shù) 46
- 默認(rèn)參數(shù)只會(huì)被賦值一次:在其所在模塊被加載的過(guò)程中,這有可能導(dǎo)致一些奇怪的現(xiàn)象。
- 使用
None作為關(guān)鍵字參數(shù)的默認(rèn)值會(huì)有一個(gè)動(dòng)態(tài)值。要在該函數(shù)的說(shuō)明文檔中詳細(xì)的記錄一下。
def log(message, when=datetime.now()):
print(‘%s: %s’ % (when, message))
log(‘Hi there!’)
sleep(0.1)
log(‘Hi again!’)
>>>
2019-1-14 22:10:10.371432: Hi there!
2019-1-14 22:10:10.371432: Hi again! # 時(shí)間并沒(méi)有變化。
# 使用None作為默認(rèn)值,文檔里要有說(shuō)明
def log(message, when=None):
"""Log a message with a timestamp.
Args:
message: Message to print
when: datetime of when the message occurred.
Default to the present time.
"""
when = datetime.now() if when is None else when
print("%s: %s" %(when, message))
# another example
def decode(data, default=None):
"""Load JSON data from string.
Args:
data: JSON data to be decoded.
default: Value to return if decoding fails.
Defaults to an empty dictionary.
"""
if default is None:
default = {}
try:
return json.loads(data)
except ValueError:
return default
第 21 條:用只能以關(guān)鍵字形式指定的參數(shù)來(lái)確保代碼明晰 49
- 關(guān)鍵字參數(shù)使得函數(shù)調(diào)用的意圖更加的清晰,明顯。
- 使用keyword-only參數(shù)可以強(qiáng)迫函數(shù)調(diào)用者提供關(guān)鍵字來(lái)賦值,這樣對(duì)于容易使人疑惑的函數(shù)參數(shù)很有效,尤其適用于接收多個(gè)布爾變量的情況。
- Python3中有明確的keyword-only函數(shù)語(yǔ)法。
- Python2中可以通過(guò)**kwargs模擬實(shí)現(xiàn)keyword-only函數(shù)語(yǔ)法,并且人工的觸發(fā)TypeError異常。
- keyword-only在函數(shù)參數(shù)列表中的位置很重要,這點(diǎn)大家尤其應(yīng)該明白!
def safe_division(number, divisor, ignore_overflow,
ignore_zero_division):
# 省略了實(shí)現(xiàn).......
result = safe_division(1, 10**500, True, False)
result = safe_division(1, 0, False, True)
# 上述函數(shù)使用上不方便,因?yàn)槿菀淄?ignore_overflow 和 ignore_zero_division 的順序
# 用 keyword 引數(shù)可解決此問(wèn)題,在 Python 3 可以宣告強(qiáng)制接收 keyword-only 參數(shù)。
def safe_division_c(number, divisor, *,
ignore_overflow=False,
ignore_zero_division=False):
#....
safe_division_c(1, 10**500, True, False)
>>>
TypeError: safe_division_c() takes 2 positional arguments but 4 were given
safe_division(1, 0, ignore_zero_division=True) # OK
Python 2 雖然沒(méi)有這種語(yǔ)法,但可以用 ** 操作符模擬
注:* 操作符接收可變數(shù)量的位置參數(shù),** 接受任意數(shù)量的關(guān)鍵字參數(shù)
# Python 2
def safe_division(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_division = kwargs.pop('ignore_zero_division', False)
if kwargs:
raise TypeError("Unexpected **kwargs: %r"%kwargs)
# ···
# test
safe_division(1, 10)
safe_division(1, 0, ignore_zero_division=True)
safe_division(1, 10**500, ignore_overflow=True)
# 而想通過(guò)位置參數(shù)賦值,就不會(huì)正常的運(yùn)行了
safe_division(1, 0, False, True)
>>>
TypeError:safe_division() takes 2 positional arguments but 4 were given.
第 3 章 類(lèi)與繼承 53
第 22 條:盡量用輔助類(lèi)來(lái)維護(hù)程序的狀態(tài),而不要用字典和元組 53
- 避免字典中嵌套字典,或者長(zhǎng)度較大的元組。
- 在一個(gè)整類(lèi)(類(lèi)似于前面第一個(gè)復(fù)雜類(lèi)那樣)之前考慮使用
namedtuple制作輕量,不易發(fā)生變化的容器。 - 當(dāng)內(nèi)部的字典關(guān)系變得復(fù)雜的時(shí)候?qū)⒋a重構(gòu)到多個(gè)工具類(lèi)中。
dictionaries 以及 tuples 拿來(lái)存簡(jiǎn)單的資料很方便,但是當(dāng)資料越來(lái)越復(fù)雜時(shí),例如多層 dictionaries 或是 n-tuples,程式的可讀性就下降了。你可以從依賴(lài)樹(shù)的底端開(kāi)始,將其劃分成多個(gè)類(lèi)。這就是代碼的設(shè)計(jì)問(wèn)題了。
第 23 條:簡(jiǎn)單的接口應(yīng)該接受函數(shù),而不是類(lèi)的實(shí)例 58
- 在Python中,不需要定義或?qū)崿F(xiàn)什么類(lèi),對(duì)于簡(jiǎn)單接口組件而言,函數(shù)就足夠了。
- Python中引用函數(shù)和方法的原因就在于它們是first-class,可以直接的被運(yùn)用在表達(dá)式中。
特殊方法__call__允許你像調(diào)用函數(shù)一樣調(diào)用一個(gè)對(duì)象實(shí)例。 - 當(dāng)你需要一個(gè)函數(shù)來(lái)維護(hù)狀態(tài)信息的時(shí)候,考慮一個(gè)定義了
__call__方法的狀態(tài)閉包類(lèi)哦(詳見(jiàn)第15項(xiàng):了解閉包是怎樣與變量作用域的聯(lián)系)
Python中的許多內(nèi)置的API都允許你通過(guò)向函數(shù)傳遞參數(shù)來(lái)自定義行為。這些被API使用的hooks將會(huì)在它們運(yùn)行的時(shí)候回調(diào)給你的代碼。例如:list類(lèi)型的排序方法中有一個(gè)可選的key 參數(shù)來(lái)決定排序過(guò)程中每個(gè)下標(biāo)的值。這里,我使用一個(gè)lambda表達(dá)式作為這個(gè)鍵鉤子,根據(jù)名字中字符的長(zhǎng)度來(lái)為這個(gè)集合排序。
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
函數(shù)可以作為鉤子來(lái)工作是因?yàn)?code>Python有first-class函數(shù):在編程的時(shí)候函數(shù),方法可以像其他的變量值一樣被引用,或者被傳遞給其他的函數(shù)。Python允許類(lèi)來(lái)定義__call__這個(gè)特殊的方法。它允許一個(gè)對(duì)象像被函數(shù)一樣來(lái)被調(diào)用。這樣的一個(gè)實(shí)例也引起了callable這個(gè)內(nèi)True的事實(shí)。
current = {'green': 12, 'blue': 3}
incremetns = [
('red', 5),
('blue', 17),
('orange', 9)
]
class BetterCountMissing(object):
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
counter()
assert callable(counter)
# 這里我使用一個(gè)BetterCountMissing實(shí)例作為defaultdict函數(shù)的默認(rèn)的hook值來(lái)追蹤缺省值被添加的次數(shù)。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
第 24 條:以 @classmethod 形式的多態(tài)去通用地構(gòu)建對(duì)象 62
-
Python的每個(gè)類(lèi)只支持單個(gè)的構(gòu)造方法,__init__ - 使用
@classmethod可以為你的類(lèi)定義可替代構(gòu)造方法的方法。 - 類(lèi)的多態(tài)為具體子類(lèi)的組合提供了一種更加通用的方式。
使用 @classmethod起到多態(tài)的效果:一個(gè)對(duì)于分層良好的類(lèi)樹(shù)中,不同類(lèi)之間相同名稱(chēng)的方法卻實(shí)現(xiàn)了不同的功能的體現(xiàn)。 下面的函數(shù) generate_inputs() 不夠一般化,只能使用 PathInputData ,如果想使用其它 InputData 的子類(lèi),必須改變函數(shù)。
class InputData(object):
def read(self):
raise NotImplementedError
class PathInputData(InputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))
問(wèn)題在于建立 InputData 子類(lèi)的物件不夠一般化,如果你想要編寫(xiě)另一個(gè) InputData 的子類(lèi)就必須重寫(xiě) read 方法幸好有 @classmethod,可以達(dá)到一樣的效果。
class GenericInputData(object):
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
class PathInputData(GenericInputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
@classmethod
def generate_inputs(cls, config):
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))
第 25 條:用 super 初始化父類(lèi) 67
-
Python的解決實(shí)例化次序問(wèn)題的方法MRO解決了菱形繼承中超類(lèi)多次被初始化的問(wèn)題。 - 總是應(yīng)該使用
super來(lái)初始化父類(lèi)。
應(yīng)該避免棱形繼承。如有會(huì)發(fā)生,說(shuō)明設(shè)計(jì)的不夠好。
第 26 條:只在使用 Mix-in 組件制作工具類(lèi)時(shí)進(jìn)行多重繼承 71
- 如果可以使用
mix-in實(shí)現(xiàn)相同的結(jié)果輸出的話(huà),就不要使用多繼承了。 - 當(dāng)
mix-in類(lèi)需要的時(shí)候,在實(shí)例級(jí)別上使用可插拔的行為可以為每一個(gè)自定義的類(lèi)工作的更好。 - 從簡(jiǎn)單的行為出發(fā),創(chuàng)建功能更為靈活的
mix-in。
如果你發(fā)現(xiàn)自己渴望隨繼承的便利和封裝,那么考慮mix-in吧。它是一個(gè)只定義了幾個(gè)類(lèi)必備功能方法的很小的類(lèi)。Mix-in類(lèi)不定義以自己的實(shí)例屬性,也不需要它們的初始化方法init被調(diào)用。Mix-in可以被分層和組織成最小化的代碼塊,方便代碼的重用。
mix-in 是可以替換的 class ,通常只定義 methods ,雖然本質(zhì)上上還是通過(guò)繼承的方式,但因?yàn)?mix-in 沒(méi)有自己的 state ,也就是說(shuō)沒(méi)有定義 attributes ,使用上更有彈性。
import json
class ToDictMixin(object):
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
# hasattr 函數(shù)動(dòng)態(tài)訪問(wèn)屬性,isinstance 函數(shù)動(dòng)態(tài)檢測(cè)對(duì)象類(lèi)型
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self._traverse(key, i) for i in value]
elif hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
else:
return value
class BinaryTree(ToDIctMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# 這下把大量的Python對(duì)象轉(zhuǎn)換到一個(gè)字典中變得容易多了。
tree = BinaryTree(10, left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
>>>
{'left': {'left': None,
'right': {'left': None, 'right': None, 'value': 9},
'value': 7},
'right': {'left': {'left': None, 'right': None, 'value': 11},
'right': None,
'value': 13},
'value': 10
}
第 27 條:多用 public 屬性,少用 private 屬性 75
- Python 編譯器無(wú)法嚴(yán)格保證 private 字段的私密性
- 不要盲目將屬性設(shè)置為 private,而是應(yīng)該從一開(kāi)始就做好規(guī)劃,并允子類(lèi)更多地訪問(wèn)超類(lèi)的內(nèi)部的API
- 應(yīng)該多用 protected 屬性,并且在文檔中把這些字段的合理用法告訴子類(lèi)的開(kāi)發(fā)者,而不要試圖用 private 屬性來(lái)限制子類(lèi)的訪問(wèn)
- 只有當(dāng)子類(lèi)不受自己控制的時(shí)候,才可以考慮使用 private 屬性來(lái)避免名稱(chēng)沖突
Python 里面沒(méi)有真正的 “private variable”,想存取都可以存取得到。一般來(lái)說(shuō) Python 慣例是在變數(shù)前加一個(gè)底線(xiàn)代表 protected variable ,作用在于提醒開(kāi)發(fā)者使用上要注意。雙底線(xiàn)的命名方式是為了避免父類(lèi)和子類(lèi)間的命名沖突,除此之外盡量避免使用這種命名。
第 28 條:繼承 collections.abc 以實(shí)現(xiàn)自定義的容器類(lèi)型 79
- 如果要定制的子類(lèi)比較簡(jiǎn)單,那就可以直接從Python的容器類(lèi)型(如list或dict)中繼承
- 想正確實(shí)現(xiàn)自定義的容器類(lèi)型,可能需要編寫(xiě)大量的特殊方法
- 編寫(xiě)自制的容器類(lèi)型時(shí),可以從collection.abc 模塊的抽象類(lèi)基類(lèi)中繼承,那些基類(lèi)能確保我們的子類(lèi)具備適當(dāng)?shù)慕涌诩靶袨?/li>
ollections.abc 里面的 abstract classes 的作用是讓開(kāi)發(fā)者方便地開(kāi)發(fā)自己的 container ,例如 list。一般情況下繼承l(wèi)ist 就ok了,但是當(dāng)結(jié)構(gòu)比較復(fù)雜的時(shí)候就需要自己自定義,例如 list 有許多 方法,要一一實(shí)現(xiàn)有點(diǎn)麻煩。
但是使用者可能想使用像 count()以及 index()等 list 的 方法 ,這時(shí)候可以使用 collections.abc的 Sequence 。子類(lèi)只要實(shí)現(xiàn) __getitem__以及 __len__, Sequence 以及提供count()以及 index()了,而且如果子類(lèi)沒(méi)有實(shí)現(xiàn)類(lèi)似 Sequence 的抽象基類(lèi)所要求的每個(gè)方法,collections.abc 模塊就會(huì)指出這個(gè)錯(cuò)誤。
第 4 章 元類(lèi)及屬性 84
第 29 條:用純屬性取代 get 和 set 方法 84
- 使用public屬性避免set和get方法,@property定義一些特別的行為
- 如果訪問(wèn)對(duì)象的某個(gè)屬性的時(shí)候,需要表現(xiàn)出特殊的行為,那就用@property來(lái)定義這種行為
- @property 方法應(yīng)該遵循最小驚訝原則,而不應(yīng)該產(chǎn)生奇怪的副作用
- 確保@property方法是快速的,如果是慢或者復(fù)雜的工作應(yīng)該放在正常的方法里面
# 不要把 java 的那一套 getter 和 setter 帶進(jìn)來(lái)
# not like this:
lass OldResistor(object):
def __init__(self, ohms):
self._ohms = ohms
def get_ohms(self):
return self._ohms
def set_ohms(self, ohms):
self._ohms = ohms
# just like this:
class Resistor(object):
def __init__(self, ohms):
self.ohms = ohms
self.voltage = 0
self.current = 0
使用@property,來(lái)綁定一些特殊操作,但是不要產(chǎn)生奇怪的副作用,比如在getter里面做一些賦值的操作
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
# 相當(dāng)于 getter
@property
def voltage(self):
return self._voltage
# 相當(dāng)于 setter
@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms
r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
# 會(huì)執(zhí)行 setter 方法
r2.voltage = 10
print('After: %5r amps' % r2.current)
第 30 條:考慮用 @property 來(lái)代替屬性重構(gòu) 88
- 使用@property給已有屬性擴(kuò)展新需求
- 可以用 @property 來(lái)逐步完善數(shù)據(jù)模型
- 當(dāng)@property太復(fù)雜了才考慮重構(gòu)
@property可以把簡(jiǎn)單的數(shù)值屬性遷移為實(shí)時(shí)計(jì)算,只定義 getter 不定義 setter 那么就是一個(gè)只讀屬性
class Bucket(object):
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return ('Bucket(max_quota=%d, quota_consumed=%d)' %
(self.max_quota, self.quota_consumed))
@property
def quota(self):
return self.max_quota - self.quota_consumed
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
# Quota being reset for a new period
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
# Quota being filled for the new period
assert self.quota_consumed = 0
self.max_quota = amount
else:
# Quota being consumed during the period
assert self.max_quota >= self,quota_consumed
self.quota_consumed += delta
這種寫(xiě)法的好處就在于:從前使用的Bucket.quota 的那些舊代碼,既不需要做出修改,也不需要擔(dān)心現(xiàn)在的Bucket類(lèi)是如何實(shí)現(xiàn)的,可以輕松無(wú)痛擴(kuò)展新功能。但是@property也不能濫用,而且@property的一個(gè)缺點(diǎn)就是無(wú)法被復(fù)用,同一套邏輯不能在不同的屬性之間重復(fù)使用如果不停的編寫(xiě)@property方法,那就意味著當(dāng)前這個(gè)類(lèi)的代碼寫(xiě)的確實(shí)很糟糕,此時(shí)應(yīng)該重構(gòu)了。
第 31 條:用描述符來(lái)改寫(xiě)需要復(fù)用的 @property 方法 92
- 如果想復(fù)用 @property 方法及其驗(yàn)證機(jī)制,那么可以自定義描述符類(lèi)
- WeakKeyDictionary 可以保證描述符類(lèi)不會(huì)泄露內(nèi)存
- 通過(guò)描述符協(xié)議來(lái)實(shí)現(xiàn)屬性的獲取和設(shè)置操作時(shí),不要糾結(jié)于
__getatttttribute__的方法的具體運(yùn)作細(xì)節(jié)
property最大的問(wèn)題是可能造成 duplicated code 這種 code smell. 可以使用 descriptor 解決,下面的程式將重復(fù)的邏輯封裝在 Grade 里面。但是這個(gè)程式根本不能用 ,因?yàn)榇嫒〉降氖?class attributes,例如 exam.writing_grade = 40其實(shí)是Exam.__dict__['writing_grade'].__set__(exam, 40),這樣所有 Exam 的 instances 都是存取到一樣的東西 ( Grade())。
class Grade(object):
def __init__(self):
self._value = 0
def __get__(self, instance, instance_type):
return self._value
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._value = value
class Exam(object):
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()
exam = Exam()
exam.writing_grade = 40
解決方式是用個(gè) dictionary 存起來(lái),這里使用 WeakKeyDictionary避免 memory leak。
from weakref import WeakKeyDictionary
class Grade(object):
def __init__(self):
self._values = WeakKeyDictionary()
def __get__(self, instance, instance_type):
if instance is None: return self
return self._values.get(instance, 0)
def __set__(self, instance, value):
if not (0 <= value <= 100):
raise ValueError('Grade must be between 0 and 100')
self._values[instance] = value
第 32 條:用 __getattr__、 __getattribute__和__setattr__ 實(shí)現(xiàn)按需生成的屬性 97
- 通過(guò)
__getttattr__和__setattr__,我們可以用惰性的方式來(lái)加載并保存對(duì)象的屬性 - 要理解
__getattr__和__getattribute__的區(qū)別:前者只會(huì)在待訪問(wèn)的屬性缺失時(shí)觸發(fā),而后者則會(huì)在每次訪問(wèn)屬性的時(shí)候觸發(fā) - 如果要在
__getattributte__和__setattr__方法中訪問(wèn)實(shí)例屬性,那么應(yīng)該直接通過(guò) super() 來(lái)做,以避免無(wú)限遞歸 - obj.name,getattr和hasattr都會(huì)調(diào)用getattribute方法,如果name不在obj.dict里面,還會(huì)調(diào)用getattr方法,如果沒(méi)有自定義getattr方法會(huì)AttributeError異常
- 只要有賦值操作(=,setattr)都會(huì)調(diào)用setattr方法(包括a = A())
__getattr__和 __getattribute__都可以動(dòng)態(tài)地存取 attributes ,不同點(diǎn)在于如果 __dict__找不到才會(huì)呼叫 __getattr__,而 __getattribute__每次都會(huì)被呼叫到。
第 33 條:用元類(lèi)來(lái)驗(yàn)證子類(lèi) 102
- 通過(guò)元類(lèi),我們可以在生成子類(lèi)對(duì)象之前,先驗(yàn)證子類(lèi)的定義是否合乎規(guī)范
- Python2 和 Python3 指定元類(lèi)的語(yǔ)法略有不同
- 使用元類(lèi)對(duì)類(lèi)型對(duì)象進(jìn)行驗(yàn)證
- Python 系統(tǒng)把子類(lèi)的整個(gè) class 語(yǔ)句體處理完畢之后,就會(huì)調(diào)用其元類(lèi)的
__new__方法
第 34 條:用元類(lèi)來(lái)注冊(cè)子類(lèi) 104
- 在構(gòu)建模塊化的 Python 程序時(shí)候,類(lèi)的注冊(cè)是一種很有用的模式
- 開(kāi)發(fā)者每次從基類(lèi)中繼承子類(lèi)的時(shí),基類(lèi)的元類(lèi)都可以自動(dòng)運(yùn)行注冊(cè)代碼
- 通過(guò)元類(lèi)來(lái)實(shí)現(xiàn)類(lèi)的注冊(cè),可以確保所有子類(lèi)都不會(huì)泄露,從而避免后續(xù)的錯(cuò)誤
第 35 條:用元類(lèi)來(lái)注解類(lèi)的屬性 108
- 借助元類(lèi),我們可以在某個(gè)類(lèi)完全定義好之前,率先修改該類(lèi)的屬性
- 描述符與元類(lèi)能夠有效的組合起來(lái),以便對(duì)某種行為做出修飾,或者在程序運(yùn)行時(shí)探查相關(guān)信息
- 如果把元類(lèi)與描述符相結(jié)合,那就可以在不使用 weakerf 模塊的前提下避免內(nèi)存泄露
第 5 章 并發(fā)及并行 112
第 36 條:用 subprocess 模塊來(lái)管理子進(jìn)程 113
- 使用 subprocess 模塊運(yùn)行子進(jìn)程管理自己的輸入和輸出流
- subprocess 可以并行執(zhí)行最大化CPU的使用
- communicate 的 timeout 參數(shù)避免死鎖和被掛起的子進(jìn)程
import subprocess
import os
proc = subprocess.Popen(
['echo', 'Hello from the child!'],
stdout=subprocess.PIPE)
out, err = proc.communicate()
print(out.decode('utf-8'))
def run_openssl(data):
env = os.environ.copy()
env['password'] = b'\xe24U\n\xd0Ql3S\x11'
proc = subprocess.Popen(
['openssl', 'enc', '-des3', '-pass', 'env:password'],
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
proc.stdin.write(data)
proc.stdin.flush() # Ensure the child gets input
return proc
def run_md5(input_stdin):
proc = subprocess.Popen(
['md5'],
stdin=input_stdin,
stdout=subprocess.PIPE)
return proc
第 37 條:可以用線(xiàn)程來(lái)執(zhí)行阻塞式 I/O,但不要用它做平行計(jì)算 117
- 因?yàn)镚IL,Python thread并不能并行運(yùn)行多段代碼
- Python保留thread的兩個(gè)原因:1.可以模擬多線(xiàn)程,2.多線(xiàn)程可以處理I/O阻塞的情況
- Python thread可以并行執(zhí)行多個(gè)系統(tǒng)調(diào)用,這使得程序能夠在執(zhí)行阻塞式I/O操作的同時(shí),執(zhí)行一些并行計(jì)算
第 38 條:在線(xiàn)程中使用 Lock 來(lái)防止數(shù)據(jù)競(jìng)爭(zhēng) 121
- 雖然Python thread不能同時(shí)執(zhí)行,但是Python解釋器還是會(huì)打斷操作數(shù)據(jù)的兩個(gè)字節(jié)碼指令,所以還是需要鎖
- thread模塊的Lock類(lèi)是Python的互斥鎖實(shí)現(xiàn)
from threading import Thread
from threading import Lock
class LockingCounter(object):
def __init__(self):
self.lock = Lock()
self.count = 0
def increment(self, offset):
with self.lock:
self.count += offset
def worker(sensor_index, how_many, counter):
for _ in range(how_many):
# Read from the sensor
counter.increment(1)
def run_threads(func, how_many, counter):
threads = []
for i in range(5):
args = (i, how_many, counter)
thread = Thread(target=func, args=args)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
how_many = 10**5
counter = LockingCounter()
run_threads(worker, how_many, counter)
print('Counter should be %d, found %d' %
(5 * how_many, counter.count))
第 39 條:用 Queue 來(lái)協(xié)調(diào)各線(xiàn)程之間的工作 124
- 管線(xiàn)是一種優(yōu)秀的任務(wù)處理方式,它可以把處理流程劃分為若干階段,并使用多條Python線(xiàn)程同時(shí)執(zhí)行這些任務(wù)
- 構(gòu)建并發(fā)式的管線(xiàn)時(shí),要注意許多問(wèn)題,包括:如何防止某個(gè)階段陷入持續(xù)等待的狀態(tài)之中、如何停止工作線(xiàn)程,以及如何防止內(nèi)存膨脹等
- Queue類(lèi)具備構(gòu)建健壯并發(fā)管道的特性:阻塞操作,緩存大小和連接(join)
from queue import Queue
from threading import Thread
class ClosableQueue(Queue):
SENTINEL = object()
def close(self):
self.put(self.SENTINEL)
def __iter__(self):
while True:
item = self.get()
try:
if item is self.SENTINEL:
return # Cause the thread to exit
yield item
finally:
self.task_done()
class StoppableWorker(Thread):
def __init__(self, func, in_queue, out_queue):
super().__init__()
self.func = func
self.in_queue = in_queue
self.out_queue = out_queue
def run(self):
for item in self.in_queue:
result = self.func(item)
self.out_queue.put(result)
def download(item):
return item
def resize(item):
return item
def upload(item):
return item
download_queue = ClosableQueue()
resize_queue = ClosableQueue()
upload_queue = ClosableQueue()
done_queue = ClosableQueue()
threads = [
StoppableWorker(download, download_queue, resize_queue),
StoppableWorker(resize, resize_queue, upload_queue),
StoppableWorker(upload, upload_queue, done_queue),
]
for thread in threads:
thread.start()
for _ in range(1000):
download_queue.put(object())
download_queue.close()
download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print(done_queue.qsize(), 'items finished')
第 40 條:考慮用協(xié)程來(lái)并發(fā)地運(yùn)行多個(gè)函數(shù) 131
- 線(xiàn)程有三個(gè)大問(wèn)題:
- 需要特定工具去確定安全性
- 單個(gè)線(xiàn)程需要8M的內(nèi)存
- 線(xiàn)程啟動(dòng)消耗
- coroutine只有1kb的內(nèi)存消耗
- generator可以通過(guò)send方法把值傳遞給yield
def my_coroutine():
while True:
received = yield
print("Received:", received)
it = my_coroutine()
next(it)
it.send("First")
('Received:', 'First')
第 41 條:考慮用 concurrent.futures 來(lái)實(shí)現(xiàn)真正的平行計(jì)算 141
- CPU瓶頸模塊使用C擴(kuò)展
- concurrent.futures的multiprocessing可以并行處理一些任務(wù),Python2沒(méi)有這個(gè)模塊
- multiprocessing 模塊所提供的那些高級(jí)功能,都特別復(fù)雜,開(kāi)發(fā)者盡量不要直接使用它們
使用 concurrent.futures 里面的 ProcessPoolExecutor 可以很簡(jiǎn)單地平行處理 CPU-bound 的程式,省得用 multiprocessing 自定義。
from concurrent.futures import ProcessPoolExecutor
start = time()
pool = ProcessPoolExecutor(max_workers=2) # The one change
results = list(pool.map(gcd, numbers))
end = time()
print('Took %.3f seconds' % (end - start))
第 6 章 內(nèi)置模塊 145
第 42 條:用 functools.wraps 定義函數(shù)修飾器 145
第 43 條:考慮以 contextlib 和 with 語(yǔ)句來(lái)改寫(xiě)可復(fù)用的 try/f inally 代碼 148
第 44 條:用 copyreg 實(shí)現(xiàn)可靠的 pickle 操作 151
第 45 條:應(yīng)該用 datetime 模塊來(lái)處理本地時(shí)間,而不是用 time 模塊 157
第 46 條:使用內(nèi)置算法與數(shù)據(jù)結(jié)構(gòu) 161
第 47 條:在重視精確度的場(chǎng)合,應(yīng)該使用 decimal 166
第 48 條:學(xué)會(huì)安裝由 Python 開(kāi)發(fā)者社區(qū)所構(gòu)建的模塊 168
第 7 章 協(xié)作開(kāi)發(fā) 170
第 49 條:為每個(gè)函數(shù)、類(lèi)和模塊編寫(xiě)文檔字符串 170
第 50 條:用包來(lái)安排模塊,并提供穩(wěn)固的 API 174
第 51 條:為自編的模塊定義根異常,以便將調(diào)用者與 API 相隔離 179
第 52 條:用適當(dāng)?shù)姆绞酱蚱蒲h(huán)依賴(lài)關(guān)系 182
第 53 條:用虛擬環(huán)境隔離項(xiàng)目,并重建其依賴(lài)關(guān)系 187
第 8 章 部署 193
第 54 條:考慮用模塊級(jí)別的代碼來(lái)配置不同的部署環(huán)境 193
第 55 條:通過(guò) repr 字符串來(lái)輸出調(diào)試信息 195
第 56 條:用 unittest 來(lái)測(cè)試全部代碼 198
第 57 條:考慮用 pdb 實(shí)現(xiàn)交互調(diào)試 201
第 58 條:先分析性能,然后再優(yōu)化 203
第 59 條:用 tracemalloc 來(lái)掌握內(nèi)存的使用及泄漏情況 208