Effective Python --編寫高質量Python代碼的59個有效方法 (讀書筆記 每日更新一條 Updated 2019.1.28)
好記性不如爛筆頭,讀讀寫寫才能記得牢固些
第 1 章 用 Pythonic 方式來思考 1
第 1 條:確認自己所用的 Python 版本 1
2to3和six等工具可以吧Python2代碼適配到Python3版本上。
- 有兩個版本的 Python 處于活躍狀態,它們是: Python 2 與 Python 3。
- 有很多種流行的 Python 運行時環境,例如, CPython、 Jython、 IronPython 以及
PyPy 等。 - 在操作系統的命令行中運行 Python 時,請確保該 Python 的版本與你想使用的
Python 版本相符。 - 由于 Python 社區把開發重點放在 Python 3 上,所以在開發后續項目時,應該優先考慮采用 Python 3。
第 2 條:遵循 PEP 8 風格指南 3
-
空白:
- 不要使用 tab 縮進,使用空格來縮進
- 使用四個空格縮進,使用四個空格對長表達式換行縮進
- 每行的字符數不應該超過 79
- class和funciton之間用兩個空行,class的method之間用一個空行
- list索引和函數調用,關鍵字參數賦值不要在兩旁加空格
- 變量賦值前后都用一個空格
-
命名
- 函數,變量以及屬性應該使用小寫,如果有多個單詞推薦使用下劃線進行連接,如lowercase_underscore
- 被保護 的屬性應該使用 單個 前導下劃線來聲明。
- 私有 的屬性應該使用 兩個 前導下劃線來進行聲明。
- 類以及異常信息 應該使用單詞 首字母大寫 形式,也就是我們經常使用的駝峰命名法,如CapitalizedWord。
- 模塊級 別的常量應該使用 全部大寫 的形式, 如ALL_CAPS。
- 類內部的實例方法的應該將self作為其第一個參數。且self也是對當前類對象的引用。
- 類方法應該使用cls來作為其第一個參數。且self引用自當前類。
-
表達式和語句( Python之禪: 每件事都應該有直白的做法,而且最好只有一種 )
- 使用內聯否定(如 if a is not b) 而不是顯示的表達式(如if not a is b)。
- 不要簡單地通過變量的長度(if len(somelist) == 0)來判斷空值。使用隱式的方式如來假設空值的情況(如if not somelist 與 False來進行比較)。
- 上面的第二條也適用于非空值(如[1],或者’hi’)。對這些非空值而言 if somelist默認包含隱式的True。
- 避免將if , for, while, except等包含多個語塊的表達式寫在一行內,應該分割成多行。
- 總是把import語句寫在Python文件的頂部。
- 當引用一個模塊的時候使用絕對的模塊名稱,而不是與當前模塊路徑相關的名稱。例如要想引入bar包下面的foo模塊,應該使用from bar import foo而不是import foo。
- 如果非要相對的引用,應該使用明確的語法from . import foo。
- 按照以下規則引入模塊:標準庫,第三方庫,你自己的庫。每一個部分內部也應該按照字母順序來引入。
第 3 條:了解 bytes、 str 與 unicode 的區別 5
- Python3 兩種字符串類型:bytes和str,bytes表示8-bit的二進制值,str表示unicode字符。開發者不能用 > 或 + 混同操作bytes和str實例
- Python2 兩種字符串類型:str和unicode,str表示8-bit的二進制值,unicode表示unicode字符。如果str只包含7位ASCII字符,那么通過相關的操作符才同時使用str與unicode
- 在對輸入的數據進行操作之前,使用輔助函數來保證字符的序列
- 從文件中讀取或者寫入二進制數據時,總應該使用 ‘rb’ 或 ‘wb’ 等二進制模式來開啟文件
第 4 條:用輔助函數來取代復雜的表達式 8
- 開發者很容易過度使用Python的語法特效,從而寫出那種特別復雜并且難以理解的單行表達式
- 請把復雜的表達式移入輔助函數中,如果要反復使用相同的邏輯,那就更應該這么做
- 使用 if/else 表達式,要比使用 or 或者 and 這樣的 Booolean 操作符更加清晰
第 5 條:了解切割序列的辦法 10
- 分片機制自動處理越界問題,但是最好在表達邊界大小范圍是更加的清晰。(如a[:20] 或者a[-20:])
- list,str,bytes和實現
__getitem__
和__setitem__
這兩個特殊方法的類都支持slice操作 - 基本形式是:somelist[start:end],不包括end,可以使用負數,-1 表示最后一個,默認正向選取,下標0可以省略,最后一個下標也可以省略
- slice list是shadow copy,somelist[0:]會復制原list,切割之后對新得到的列表進行修改不會影響原來的列表
- slice賦值會修改slice list,即使長度不一致(增刪改)
第 6 條:在單次切片操作內,不要同時指定 start、 end 和 stride 13
- 既有start和end, 又有stride的切割操作,會讓人感到困惑。
- 盡量使用stride為正數,且不帶start或者end索引的切割操作。盡可能的避免在分片中使用負數值。
- 避免在分片中同時使用start,end,stride;如果非要使用,考慮兩次賦值(一個分片,一個調幅),或者使用內置模塊itertoolsde 的 islice方法來進行處理。
第 7 條:用列表推導來取代 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()}
- 列表表達式比內置的
map
,filter
更加清晰,因為map
,filter
需要額外的lambda
表達式的支持。 - 列表表達式允許你很容易的跳過某些輸入值,而一個
map
沒有filter
幫助的話就不能完成這一個功能。 - 字典和集合也都支持列表表達式
第 8 條:不要使用含有兩個以上表達式的列表推導 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)
- 列表表達式支持多層的循環和條件語句,以及每層循環內部的條件語句。
- 當列表表達式內部多余兩個表達式的時候就會變得難于閱讀,這種寫法應該避免使用。
第 9 條:用生成器表達式來改寫數據量較大的列表推導 18
列表生成式的缺點是,在推倒過程中,對輸入列表中的每一個值,可能都要創建只包含一個元素的新列表。這對于小的輸入序列可能是很好用的,但是大的輸入序列而言就很有可能導致你的程序崩潰。
ython提供了一個generator expression(生成器表達式),在程序運行的過程中,生成其表達式不實現整個輸出序列,相反,生成其表達式僅僅是對從表達式中產生一個項目的迭代器進行計算,說白了就是每次僅僅處理一個迭代項,而不是整個序列。
生成器表達式通過使用類似于列表表達式的語法(在()之間而不是[]之間,僅此區別)來創建。
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))
- 當遇到大輸入事件的時候,使用列表表達式可能導致一些問題。
- 生成器表達式通過迭代的方式來處理每一個列表項,可以防止出現內存危機。
- 當生成器表達式 處于鏈式狀態時,會執行的很迅速。
第 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
提供了簡潔的語法,再循環迭代一個迭代器的同時既能獲取下標,也能獲取當前值。 - 可以添加第二個參數來指定索引開始的序號,默認為
0
第 11 條:用 zip 函數同時遍歷兩個迭代器 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
- 內置的zip函數可以并行的對多個迭代器進行處理。
- 在Python3中,zip 相當于生成器,會在遍歷過程中逐次產生元組。而在Python2中,zip返回的是一個包含了其處理好的所有元祖的一個集合。
- 如果所處理的迭代器的長度不一致時,zip會默認截斷輸出,使得長度為最先到達尾部的那個長度。
- 內置模塊itertools中的zip_longest函數可以并行地處理多個迭代器,而可以無視長度不一致的問題。
第 12 條:不要在 for 和 while 循環后面寫 else 塊 23
for i in range(2):
print('Loop %d' % i)
else:
print('Else block')
>>>
Loop 0
Loop 1
Else block
-
Python
有用特殊的語法能夠讓else
語塊在循環體結束的時候立刻得到執行。 - 循環體后的
else
語塊只有在循環體沒有觸發break
語句的時候才會執行。 - 避免在循環體的后面使用
else
語塊,因為這樣的表達不直觀,而且容易誤導讀者。
第 13 條:合理利用 try/except/else/f inally 結構中的每個代碼塊 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組合語句可以使得你的代碼變得很整潔而無視try塊中是否發生異常。
- else塊可以最大限度的減少try塊中的代碼的長度,并且可以可視化地辨別try/except成功運行的部分。
- else塊經常會被用于在try塊成功運行后添加額外的行為,但是要確保代碼會在finally塊之前得到運行。
finally 塊: 總是會執行,可以用來關閉文件句柄之類的
else 塊 : try 塊沒有發生異常則執行 else 塊,有了 else 塊,我們可以盡量減少 try 塊的代碼量
第 2 章 函數 28
第 14 條:盡量用異常來表示特殊情況,而不要返回 None 28
- 返回
None
的函數來作為特殊的含義是容易出錯的,因為None
和其他的變量(例如zero
,空字符串)在條件表達式的判斷情景下是等價的。 - 通過觸發一個異常而不是直接的返回
None
是比較常用的一個方法。這樣調用方就能夠合理地按照函數中的說明文檔來處理由此而引發的異常了。
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支持閉包。閉包是一種定義在某個作用域中的函數,這種函數引用了作用域中的變量。
Python的函數是一級對象,我們可以直接引用函數,把函數賦值給變量、當成參數傳遞給其它函數等等。。
Python編譯器變量查找域的順序:
- 當前函數的作用域
- 任何其他的封閉域(比如其他的包含著的函數)。
- 包含該段代碼的模塊域(也稱之為全局域)
- 內置域(包含了像len,str等函數的域)
# 優先排序
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的作用域就會由helper轉至sort_priority2函數
values.sort(key=helper)
return found
-
獲取閉包中的數據-Python3
def srt_priority3(numbers, group): found = False def helper(x): nonlocal found # 表明found是閉包外數據 if x in group: found = True return (0, x) return (1, x) numbers.sort(key=helper) return found
當數據在閉包外將被賦值到另一個域時,nonlocal 語句使得這個過程變得很清晰。它也是對global語句的一個補充,可以明確的表明變量的賦值應該被直接放置到模塊域中。
然而,像這樣的反模式,對使用在那些簡單函數之外的其他的任何地方。nonlocal引起的副作用是難以追蹤的,而在那些包含著nonlocal語句和賦值語句交叉聯系的大段代碼的函數的內部則尤為明顯。
當你感覺自己的nonlocal語句開始變的復雜的時候,我非常建議你重構一下代碼,寫成一個工具類。這里,我定義了一個實現了與上面的那個函數功能相一致的工具類。雖然有點長,但是代碼卻變得更加的清晰了(詳見第23項:對于簡單接口使用函數而不是類里面的
__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
關鍵字的。為了實現相似的功能,你需要廣泛的借助于Python
的作用與域規則。雖然這個方法并不是完美的,但是這是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 將會橫向查找該變量所在的域來分析其當前值。技巧就是發現的值是一個易變的列表。這意味著一旦檢索,閉包就可以修改found的狀態值,并且把內部數據的改變發送到外部,這也就打破了閉包引發的局部變量作用域無法被改變的難題。其根本還是在于列表本身元素值可以被改變,這才是此函數可以正常工作的關鍵。
當found為一個dictionary類型的時候,也是可以正常工作的,原理與上文所言一致。此外,found還可以是一個集合,一個你自定義的類等等。
要點:
- 閉包函數可以從變量被定義的作用域內引用變量。
- 默認地,閉包不能通過賦值來影響其檢索域。
- 在Python3中,可以使用nonlocal關鍵字來突破閉包的限制,進而在其檢索域內改變其值。(global 關鍵字用于使用全局變量,nonlocal 關鍵字用于使用局部變量(函數內))
- Python2中沒有nonlocal關鍵字,替代方案就是使用一個單元素(如列表,字典,集合等等)來實現與nonlocal一致的功能。
- 除了簡單的函數,在其他任何地方都應該盡力的避免使用nonlocal關鍵字。
第 16 條:考慮用生成器來改寫直接返回列表的函數 35
- 相較于返回一個列表的情況,替代方案中使用生成器可以使得代碼變得更加的清晰。
- 生成器返回的迭代器,是在其生成器內部一個把值傳遞給了
yield
變量的集合。 - 生成器可以處理很大的輸出序列就是因為它在處理的時候不會完全的包含所有的數據。
#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 條:在參數上面迭代時,要多加小心 37
- 多次遍歷輸入參數的時候應該多加小心。如果參數是迭代器的話你可能看到奇怪的現象或者缺少值現象的發生。
- Python的iterator協議定義了容器和迭代器在iter和next下對于循環和相關表達式的關系。
只要實現了__iter__
方法,你就可以很容易的定義一個可迭代的容器類。 - 通過連續調用兩次iter方法,你就可以預先檢測一個值是不是迭代器而不是容器。兩次結果一致那就是迭代器,否則就是容器了。調用內置的next函數,可以令迭代器前進一步。
#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)) # 這里其實已經執行到頭了
>>>
[15, 35, 80]
[]
# 如何解決?每次調用都創建iterator避免上面list分配內存
def normalize_func(get_iter): # get_iter 是函數
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循環會調用內置iter函數,進而調用對象的__iter__
方法,__iter__
會返回iterator對象(實現__next__
方法)。用iter函數檢測iterator:
class ReadVists(object):
def __int__(self, data_path):
self.data_path = data_path
# 在自己的類中把__iter__實現為生成器,就可以實現一個可以迭代的容器類
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): # 是個迭代器
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
# 但是如果輸入值不是一個容器類的話,就會引發異常了
it = iter(visits)
normalize_defensive(it)
>>>
TypeError: Must supply a container
第 18 條:用數量可變的位置參數減少視覺雜訊 41
- 通過使用
*args
定義語句,函數可以接收可變數量的位置參數。 - 你可以通過
*
操作符來將序列中的元素作為位置變量。 - 帶有
*
操作符的生成器變量可能會引起程序的內存溢出,或者機器宕機。 - 為可以接受
*args
的函數添加新的位置參數可以產生難于發現的問題,應該謹慎使用。
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 條:用關鍵字參數來表達可選的行為 43
- 函數的參數值即可以通過位置被指定,也可以通過關鍵字來指定。
- 相較于使用位置參數賦值,使用關鍵字來賦值會讓你的賦值語句邏輯變得更加的清晰。
- 帶有默認參數的關鍵字參數函數可以很容易的添加新的行為,尤其適合向后兼容。
- 可選的關鍵字參數應該優于位置參數被考慮使用。
關鍵字參數的好處:
- 代碼可讀性的提高
- 以在定義的時候初始化一個默認值
- 在前面的調用方式不變的情況下可以很好的拓展函數的參數,不用修改太多的代碼
# 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 和文檔字符串來描述具有動態默認值的參數 46
- 默認參數只會被賦值一次:在其所在模塊被加載的過程中,這有可能導致一些奇怪的現象。
- 使用
None
作為關鍵字參數的默認值會有一個動態值。要在該函數的說明文檔中詳細的記錄一下。
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! # 時間并沒有變化。
# 使用None作為默認值,文檔里要有說明
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 條:用只能以關鍵字形式指定的參數來確保代碼明晰 49
- 關鍵字參數使得函數調用的意圖更加的清晰,明顯。
- 使用keyword-only參數可以強迫函數調用者提供關鍵字來賦值,這樣對于容易使人疑惑的函數參數很有效,尤其適用于接收多個布爾變量的情況。
- Python3中有明確的keyword-only函數語法。
- Python2中可以通過**kwargs模擬實現keyword-only函數語法,并且人工的觸發TypeError異常。
- keyword-only在函數參數列表中的位置很重要,這點大家尤其應該明白!
def safe_division(number, divisor, ignore_overflow,
ignore_zero_division):
# 省略了實現.......
result = safe_division(1, 10**500, True, False)
result = safe_division(1, 0, False, True)
# 上述函數使用上不方便,因為容易忘記 ignore_overflow 和 ignore_zero_division 的順序
# 用 keyword 引數可解決此問題,在 Python 3 可以宣告強制接收 keyword-only 參數。
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 雖然沒有這種語法,但可以用 **
操作符模擬
注:*
操作符接收可變數量的位置參數,**
接受任意數量的關鍵字參數
# 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)
# 而想通過位置參數賦值,就不會正常的運行了
safe_division(1, 0, False, True)
>>>
TypeError:safe_division() takes 2 positional arguments but 4 were given.
第 3 章 類與繼承 53
第 22 條:盡量用輔助類來維護程序的狀態,而不要用字典和元組 53
- 避免字典中嵌套字典,或者長度較大的元組。
- 在一個整類(類似于前面第一個復雜類那樣)之前考慮使用
namedtuple
制作輕量,不易發生變化的容器。 - 當內部的字典關系變得復雜的時候將代碼重構到多個工具類中。
dictionaries 以及 tuples 拿來存簡單的資料很方便,但是當資料越來越復雜時,例如多層 dictionaries 或是 n-tuples,程式的可讀性就下降了。你可以從依賴樹的底端開始,將其劃分成多個類。這就是代碼的設計問題了。
第 23 條:簡單的接口應該接受函數,而不是類的實例 58
- 在Python中,不需要定義或實現什么類,對于簡單接口組件而言,函數就足夠了。
- Python中引用函數和方法的原因就在于它們是first-class,可以直接的被運用在表達式中。
特殊方法__call__
允許你像調用函數一樣調用一個對象實例。 - 當你需要一個函數來維護狀態信息的時候,考慮一個定義了
__call__
方法的狀態閉包類哦(詳見第15項:了解閉包是怎樣與變量作用域的聯系)
Python中的許多內置的API都允許你通過向函數傳遞參數來自定義行為。這些被API使用的hooks將會在它們運行的時候回調給你的代碼。例如:list類型的排序方法中有一個可選的key 參數來決定排序過程中每個下標的值。這里,我使用一個lambda表達式作為這個鍵鉤子,根據名字中字符的長度來為這個集合排序。
names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
函數可以作為鉤子來工作是因為Python
有first-class
函數:在編程的時候函數,方法可以像其他的變量值一樣被引用,或者被傳遞給其他的函數。Python
允許類來定義__call__
這個特殊的方法。它允許一個對象像被函數一樣來被調用。這樣的一個實例也引起了callable
這個內True
的事實。
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)
# 這里我使用一個BetterCountMissing實例作為defaultdict函數的默認的hook值來追蹤缺省值被添加的次數。
counter = BetterCountMissing()
result = defaultdict(counter, current)
for key, amount in increments:
result[key] += amount
assert counter.added == 2
第 24 條:以 @classmethod 形式的多態去通用地構建對象 62
-
Python
的每個類只支持單個的構造方法,__init__
- 使用
@classmethod
可以為你的類定義可替代構造方法的方法。 - 類的多態為具體子類的組合提供了一種更加通用的方式。
使用 @classmethod起到多態的效果:一個對于分層良好的類樹中,不同類之間相同名稱的方法卻實現了不同的功能的體現。 下面的函數 generate_inputs() 不夠一般化,只能使用 PathInputData ,如果想使用其它 InputData 的子類,必須改變函數。
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))
問題在于建立 InputData
子類的物件不夠一般化,如果你想要編寫另一個 InputData
的子類就必須重寫 read
方法幸好有 @classmethod
,可以達到一樣的效果。
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 初始化父類 67
-
Python
的解決實例化次序問題的方法MRO
解決了菱形繼承中超類多次被初始化的問題。 - 總是應該使用
super
來初始化父類。
應該避免棱形繼承。如有會發生,說明設計的不夠好。
第 26 條:只在使用 Mix-in 組件制作工具類時進行多重繼承 71
- 如果可以使用
mix-in
實現相同的結果輸出的話,就不要使用多繼承了。 - 當
mix-in
類需要的時候,在實例級別上使用可插拔的行為可以為每一個自定義的類工作的更好。 - 從簡單的行為出發,創建功能更為靈活的
mix-in
。
如果你發現自己渴望隨繼承的便利和封裝,那么考慮mix-in吧。它是一個只定義了幾個類必備功能方法的很小的類。Mix-in類不定義以自己的實例屬性,也不需要它們的初始化方法init被調用。Mix-in可以被分層和組織成最小化的代碼塊,方便代碼的重用。
mix-in 是可以替換的 class ,通常只定義 methods ,雖然本質上上還是通過繼承的方式,但因為 mix-in 沒有自己的 state ,也就是說沒有定義 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 函數動態訪問屬性,isinstance 函數動態檢測對象類型
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對象轉換到一個字典中變得容易多了。
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 編譯器無法嚴格保證 private 字段的私密性
- 不要盲目將屬性設置為 private,而是應該從一開始就做好規劃,并允子類更多地訪問超類的內部的API
- 應該多用 protected 屬性,并且在文檔中把這些字段的合理用法告訴子類的開發者,而不要試圖用 private 屬性來限制子類的訪問
- 只有當子類不受自己控制的時候,才可以考慮使用 private 屬性來避免名稱沖突
Python 里面沒有真正的 “private variable”,想存取都可以存取得到。一般來說 Python 慣例是在變數前加一個底線代表 protected variable ,作用在于提醒開發者使用上要注意。雙底線的命名方式是為了避免父類和子類間的命名沖突,除此之外盡量避免使用這種命名。
第 28 條:繼承 collections.abc 以實現自定義的容器類型 79
- 如果要定制的子類比較簡單,那就可以直接從Python的容器類型(如list或dict)中繼承
- 想正確實現自定義的容器類型,可能需要編寫大量的特殊方法
- 編寫自制的容器類型時,可以從collection.abc 模塊的抽象類基類中繼承,那些基類能確保我們的子類具備適當的接口及行為
ollections.abc
里面的 abstract classes 的作用是讓開發者方便地開發自己的 container ,例如 list。一般情況下繼承list 就ok了,但是當結構比較復雜的時候就需要自己自定義,例如 list 有許多 方法,要一一實現有點麻煩。
但是使用者可能想使用像 count()以及 index()等 list 的 方法 ,這時候可以使用 collections.abc的 Sequence 。子類只要實現 __getitem__
以及 __len__
, Sequence 以及提供count()以及 index()了,而且如果子類沒有實現類似 Sequence 的抽象基類所要求的每個方法,collections.abc 模塊就會指出這個錯誤。
第 4 章 元類及屬性 84
第 29 條:用純屬性取代 get 和 set 方法 84
- 使用public屬性避免set和get方法,@property定義一些特別的行為
- 如果訪問對象的某個屬性的時候,需要表現出特殊的行為,那就用@property來定義這種行為
- @property 方法應該遵循最小驚訝原則,而不應該產生奇怪的副作用
- 確保@property方法是快速的,如果是慢或者復雜的工作應該放在正常的方法里面
# 不要把 java 的那一套 getter 和 setter 帶進來
# 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,來綁定一些特殊操作,但是不要產生奇怪的副作用,比如在getter里面做一些賦值的操作
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0
# 相當于 getter
@property
def voltage(self):
return self._voltage
# 相當于 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)
# 會執行 setter 方法
r2.voltage = 10
print('After: %5r amps' % r2.current)
第 30 條:考慮用 @property 來代替屬性重構 88
- 使用@property給已有屬性擴展新需求
- 可以用 @property 來逐步完善數據模型
- 當@property太復雜了才考慮重構
@property可以把簡單的數值屬性遷移為實時計算,只定義 getter 不定義 setter 那么就是一個只讀屬性
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
這種寫法的好處就在于:從前使用的Bucket.quota 的那些舊代碼,既不需要做出修改,也不需要擔心現在的Bucket類是如何實現的,可以輕松無痛擴展新功能。但是@property也不能濫用,而且@property的一個缺點就是無法被復用,同一套邏輯不能在不同的屬性之間重復使用如果不停的編寫@property方法,那就意味著當前這個類的代碼寫的確實很糟糕,此時應該重構了。
第 31 條:用描述符來改寫需要復用的 @property 方法 92
- 如果想復用 @property 方法及其驗證機制,那么可以自定義描述符類
- WeakKeyDictionary 可以保證描述符類不會泄露內存
- 通過描述符協議來實現屬性的獲取和設置操作時,不要糾結于
__getatttttribute__
的方法的具體運作細節
property
最大的問題是可能造成 duplicated code 這種 code smell. 可以使用 descriptor 解決,下面的程式將重復的邏輯封裝在 Grade 里面。但是這個程式根本不能用 ,因為存取到的是 class attributes,例如 exam.writing_grade = 40
其實是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
解決方式是用個 dictionary 存起來,這里使用 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__
實現按需生成的屬性 97
- 通過
__getttattr__
和__setattr__
,我們可以用惰性的方式來加載并保存對象的屬性 - 要理解
__getattr__
和__getattribute__
的區別:前者只會在待訪問的屬性缺失時觸發,而后者則會在每次訪問屬性的時候觸發 - 如果要在
__getattributte__
和__setattr__
方法中訪問實例屬性,那么應該直接通過 super() 來做,以避免無限遞歸 - obj.name,getattr和hasattr都會調用getattribute方法,如果name不在obj.dict里面,還會調用getattr方法,如果沒有自定義getattr方法會AttributeError異常
- 只要有賦值操作(=,setattr)都會調用setattr方法(包括a = A())
__getattr__
和 __getattribute__
都可以動態地存取 attributes ,不同點在于如果 __dict__
找不到才會呼叫 __getattr__
,而 __getattribute__
每次都會被呼叫到。
第 33 條:用元類來驗證子類 102
- 通過元類,我們可以在生成子類對象之前,先驗證子類的定義是否合乎規范
- Python2 和 Python3 指定元類的語法略有不同
- 使用元類對類型對象進行驗證
- Python 系統把子類的整個 class 語句體處理完畢之后,就會調用其元類的
__new__
方法
第 34 條:用元類來注冊子類 104
- 在構建模塊化的 Python 程序時候,類的注冊是一種很有用的模式
- 開發者每次從基類中繼承子類的時,基類的元類都可以自動運行注冊代碼
- 通過元類來實現類的注冊,可以確保所有子類都不會泄露,從而避免后續的錯誤
第 35 條:用元類來注解類的屬性 108
- 借助元類,我們可以在某個類完全定義好之前,率先修改該類的屬性
- 描述符與元類能夠有效的組合起來,以便對某種行為做出修飾,或者在程序運行時探查相關信息
- 如果把元類與描述符相結合,那就可以在不使用 weakerf 模塊的前提下避免內存泄露
第 5 章 并發及并行 112
第 36 條:用 subprocess 模塊來管理子進程 113
- 使用 subprocess 模塊運行子進程管理自己的輸入和輸出流
- subprocess 可以并行執行最大化CPU的使用
- communicate 的 timeout 參數避免死鎖和被掛起的子進程
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 條:可以用線程來執行阻塞式 I/O,但不要用它做平行計算 117
- 因為GIL,Python thread并不能并行運行多段代碼
- Python保留thread的兩個原因:1.可以模擬多線程,2.多線程可以處理I/O阻塞的情況
- Python thread可以并行執行多個系統調用,這使得程序能夠在執行阻塞式I/O操作的同時,執行一些并行計算
第 38 條:在線程中使用 Lock 來防止數據競爭 121
- 雖然Python thread不能同時執行,但是Python解釋器還是會打斷操作數據的兩個字節碼指令,所以還是需要鎖
- thread模塊的Lock類是Python的互斥鎖實現
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 來協調各線程之間的工作 124
- 管線是一種優秀的任務處理方式,它可以把處理流程劃分為若干階段,并使用多條Python線程同時執行這些任務
- 構建并發式的管線時,要注意許多問題,包括:如何防止某個階段陷入持續等待的狀態之中、如何停止工作線程,以及如何防止內存膨脹等
- Queue類具備構建健壯并發管道的特性:阻塞操作,緩存大小和連接(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 條:考慮用協程來并發地運行多個函數 131
- 線程有三個大問題:
- 需要特定工具去確定安全性
- 單個線程需要8M的內存
- 線程啟動消耗
- coroutine只有1kb的內存消耗
- generator可以通過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 來實現真正的平行計算 141
- CPU瓶頸模塊使用C擴展
- concurrent.futures的multiprocessing可以并行處理一些任務,Python2沒有這個模塊
- multiprocessing 模塊所提供的那些高級功能,都特別復雜,開發者盡量不要直接使用它們
使用 concurrent.futures
里面的 ProcessPoolExecutor 可以很簡單地平行處理 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 章 內置模塊 145
第 42 條:用 functools.wraps 定義函數修飾器 145
第 43 條:考慮以 contextlib 和 with 語句來改寫可復用的 try/f inally 代碼 148
第 44 條:用 copyreg 實現可靠的 pickle 操作 151
第 45 條:應該用 datetime 模塊來處理本地時間,而不是用 time 模塊 157
第 46 條:使用內置算法與數據結構 161
第 47 條:在重視精確度的場合,應該使用 decimal 166
第 48 條:學會安裝由 Python 開發者社區所構建的模塊 168
第 7 章 協作開發 170
第 49 條:為每個函數、類和模塊編寫文檔字符串 170
第 50 條:用包來安排模塊,并提供穩固的 API 174
第 51 條:為自編的模塊定義根異常,以便將調用者與 API 相隔離 179
第 52 條:用適當的方式打破循環依賴關系 182
第 53 條:用虛擬環境隔離項目,并重建其依賴關系 187
第 8 章 部署 193
第 54 條:考慮用模塊級別的代碼來配置不同的部署環境 193
第 55 條:通過 repr 字符串來輸出調試信息 195
第 56 條:用 unittest 來測試全部代碼 198
第 57 條:考慮用 pdb 實現交互調試 201
第 58 條:先分析性能,然后再優化 203
第 59 條:用 tracemalloc 來掌握內存的使用及泄漏情況 208