第一章 用Pythonic的方式來思考
1. 確認自己所用的python版本
# 查看版本
python --version
# 在程序中使用sys模塊查詢相關的值
import sys
print(sys.version_info)
print(sys.version)
2. 遵循PEP8風格指南
《Python Enhancement Proposal #8》(8號Python增強提案),又叫PEP8,它是針對Python風格代碼而編訂的風格指南。
pylint工具是一款流行的python源碼靜態分析工具,它可以自動檢查受測代碼是否符合PEP8風格指南,而且還能找出程序的多種常見錯誤。
下面列出幾條絕對應該遵守的規則:
- 空白(whitespace):空白會影響代碼的含義和代碼清晰程度。
- 使用4個空格(space)表示縮進,不要用tab
- 每一行字符數不超過79
- 對于多行的長表達式,除了首行之外的其余各行都應該在通常的縮進級別之上在加4個空格。
- 文件中的函數和類之間應該用兩個空行隔開。
- 同一個類中,各方法之間用一個空行隔開。
- 在使用下標來獲取列表元素、調用函數或給關鍵字參數賦值時,不要在兩旁加空格。
- 為變量賦值時,賦值符號的左側和右側應該各自加一個空格,而且只寫一個就好。
- 命名:PEP8提倡采用不同的命名風格來編寫程序,以便根據這些名稱看出它們的角色。
- 函數、變量和屬性應該用小寫字母來拼寫,各單詞之間用下劃線相連,如lowercase_underscore。
- 受保護的實例屬性,應該以單個下劃線開頭,如_leading_underscore。
- 私有的實例屬性,應該以兩個下劃線開頭,如__double_leading_underscore。
- 類與異常應該以每個單詞首字母均大寫的形式來命名,如CapitalizedWord。
- 模塊級別的常量,應該全部采用大寫字母來拼寫,各單詞之間以下劃線相連,如ALL_CAPS.
- 類中的實例方法,應該把首個參數命名為self,以表示該對象自身。
- 類方法的首個參數,應該命名為cls,以表示該類自身。
- 表達式和語句:《The Zen of Python》(python之禪)說:每件事都應該有直白的做法,而且最好只有一種。
- 采用內聯形式的否定詞,而不要把否定詞放在表達式前面。如應該寫
if a is not b
而不是if not a is b
。 - 不要通過檢測長度的方法(如
if len(sonelist) == 0
)來判斷somelist是否為[]
或''
等空值,而是應該采用if not somelist
這種寫法來判斷,它會假定:空值將自動評估為False。 - 不要編寫單行的
if
語句、for
循環、while
循環及except
符合語句,而是應該把這些語句分成多行來書寫,以示清晰。 -
import
語句應該總是放在文件開頭。 - 引入模塊時應使用絕對名稱,而不應該根據當前模塊的路徑來使用相對名稱。如引入bar包中的foo模塊時,應完整的寫出
from bar import foo
, 而不是import foo
。 - 如果一定要以相對名稱來編寫import語句,那就采用明確的寫法:
from . import foo
。 - 文件中的那些import語句應該按順序劃分成3個部分,分別表示標準庫模塊、第三方模塊以及自用模塊。每一部分中,各import語句應該按模塊的字母順序來排列。
3. 了解bytes、str與Unicode的區別。
4. 用輔助函數來取代復雜的表達式。
- 開發者很容易過度使用Python的語法特性,從而寫出那種特別復雜并且難以理解的單行表達式。
- 如果表達式比較復雜,要考慮將其拆解成小塊,并把這些邏輯移入輔助函數中,如果反復使用相同的邏輯,那就更應該這么做。這會令代碼更加易讀,比原來的密集寫法要更好。
- 使用if/else表達式要比用or或and這樣的Boolen操作符寫成的表達式更加清晰。
5.了解切割序列的辦法。
切片操作(slice)可以輕易訪問序列中的子集。所有實現了getitem和setitem這兩個特殊方法的python類均可使用切片,如list、str、bytes等。
- 不要寫多余的代碼,當start索引為0或end索引為序列長度時,應該將其忽略。
- 切片操作不會計較start和end索引是否越界,這使得我們很容易就能從序列的前端或后端開始,對其進行范圍固定的切片操作,如
a[:20],a[-20:]
。 - 對list賦值的時候,如果使用切片操作,就會把原列表中處在相關范圍內的值替換成新值,即使它們的長度不同也可以替換。如:
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
a[2:7] = [99, 22, 14]
print('After ', a)
->After ['a', 'b', 99, 22, 14, 'h']
6. 在單次切片內,不要同時指定start、end和stride。
7. 用列表推導式來取代map和filter。
- 列表推導式要比內置的map和filter函數更清晰,因為它無需額外編寫lambda表達式。
- 列表推導式可以跳過輸入列表中的某些元素,如果改用map來做,那就必須輔以filter方能實現。
- 字典與集也支持推導表達式。
8. 不要使用含有兩個以上表達式的列表推導。
- 列表推導支持多級循環,每一級循環也支持多項條件。但最好不要使用兩個以上的表達式。可以使用兩個條件、兩個循環或一個條件搭配一個循環。如果要寫的代碼比這還復雜,那就應該使用普通的if和for語句,并編寫輔助函數。
- 超過兩個表達式的列表推導式是很難理解的,應該盡量避免。
# 把矩陣簡化成一維列表,使原來的每個單元格都成為新列表的普通元素,可以使用兩個for表達式的列表推導來實現
# 這些for表達式會按照從左到右的順序來評估。
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# 另一種包含多重循環的合理用法是根據輸入列表來創建有兩層深度的新列表。
squared = [[x**2 for x in row] for row in matrix]
# 如果在多一層循環,那么列表推導式會很長。
my_list = [
[[1, 2, 3], [4, 5, 6],
#...
]
flat = [x for sublist1 in my_lists
for sublist2 in sublist1
for x in sublist2]
# 這時不如采用循環語句來實現相同效果。
flat = []
for sublist1 in my_list:
for sublist2 in sublist1:
flat.extend(sublist2)
# 列表頁支持多個if條件,處在同一循環中的多項條件,彼此之間默認形成and表達式。
a = [1, 2, 3, 4, 5, 6, 7, 8, 9,10]
b = [x for x in a if x > 4 if x % 2 == 0]
c = [x for x in a if x > 4 and x % 2 ==0]
# 同樣條件增加,會導致代碼很難懂。
9. 用生成器表達式來改寫數據量較大的列表推導
- 列表推導的缺點是:在推導過程中,對于輸入序列中的每個值來說,可能都要創建僅含一項元素的全新列表。如果輸入的數據非常多,可能會消耗大量內存,并導致程序崩潰。
- 為了解決此問題,python提供了生成器表達式(generator expression),它是對列表推導和生成器的一種泛化(generalization)。生成器表達式在運行的時候,并不會把整個輸出序列都呈現出來,而是會估值為迭代器(iterator),每個迭代器每次可以根據生成器表達式產生一項數據,避免內存過度使用。
- 生成器表達式的實現和推導式相似,只需要將列表推導的寫法用圓括號括起來即可。對生成器表達式求值時會返回一個迭代器,而不會深入處理文件的內容。
- 把某個生成器表達式所返回的迭代器放在另一個生成器表達式的for子表達式中,即可將二者結合起來。
- 串在一起的生成器表達式執行速度很快。要注意,由生成器表達式返回的迭代器是由狀態的,用過一輪后,就不要反復使用了
it = (len(x) for x in open('/tmp/my_file.txt'))
print(it) #<generator object <genexpr> at 0x101b81480>
#逐漸調用內置的next函數,即可生成下一個值
print(next(it)) #100
print(next(it)) #57
# 生成器可以互相組合
roots = ((x, x**0.5) for x in it)
#外圍的迭代器推進時會推動內部的迭代器,就產生了連鎖效應
print(next(roots)) #(15, 3.872983346207417)
10. 盡量用enumerate取代range
Python提供了內置的enumerate函數,可以把各種迭代器包裝為生成器,以便稍后產生輸出值。生成器每次產生一對輸出值,前者表示循環下標,后者表示迭代器中獲取的下一個序列元素。
- enumerate函數提供了一種精簡的寫法,可以在遍歷迭代器時獲知每個元素的索引
- 盡量用enumerate來改寫那種將range與下標訪問相結合的序列遍歷代碼
- 可以給enumerate提供第二個參數,一直盯開始計數時所使用的值
flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
# 使用range方式遍歷元素和索引
for i in range(len(flavor_list)):
print('%d: %s' % (i+1, flavor_list[i]))
# 使用enumerate方式來遍歷元素和索引
for i, flavor in enumerate(flavor_list):
print('%d: %s' % (i+1, flavor))
# 指定enumerate函數開始計數時所用的值為1,默認為0
for i, flavor in enumerate(flavor_list, 1):
print('%d: %s' % (i, flavor))
11. 用zip函數同時遍歷兩個迭代器。
- 不同的列表可能存在互相關聯。如果想平行的迭代兩份列表,可以使用zip函數。
- zip函數可以把兩個或兩個以上的迭代器封裝為生成器,會從每個迭代器中獲取該迭代器的下一個值,然后將這些值匯聚成元組。
- 如果提供的迭代器長度不等,zip會提前終止。
- itertools模塊中的zip_longest函數可以平行遍歷多個迭代器,而不用在乎他們的長度是否相等。
for name, count in zip(names, letters):
print(name)
12.不要在for和while循環后面寫else塊。
- Python有種特殊語法,可在for和while循環的內部語句塊之后緊跟一個else塊。
- 只有循環主體內沒有break,循環后面的else都會執行。如果遇見break會跳出循環,而不再執行else塊。這和if/else等語法不一樣。
- 不要在循環塊后面使用else塊,因為這種寫法既不直觀,又易使人誤解。可以使用輔助函數改進。
# if/else,try/except/else語句塊中,else的意思是如果前面的不執行,則執行else塊。
# 但for/else意思與人們的理解正好相反
for i in range(3):
print('loop %d' % i)
if i == 1:
break
else:
print('Else block!')
>>> loop 0
loop 1
# for循環遍歷的序列是空,則立刻執行else塊。
for x in []:
print('Never runs')
else:
print('For Else block!')
>>> For Else block!
# while循環的初始條件為false,后面跟著else,也會立即執行
while False:
print('Never runs')
else:
print('For Else block!')
>>> For Else block!
13. 合理利用try/except/else/finally結構中的每個代碼塊。
- finally塊。如果既要將異常向上傳播,又要在異常發生時執行清理工作,可以使用try/finally結構。這種結構的一種常見用途是確保程序能夠可靠關閉文件句柄。
#open方法必須放在try外面,因為如果打開文件時異常,程序應該跳過finally塊。
handle = open('/tmp/random_data.txt') # May raise IOError
try:
data = handle.read() # May raise UnicodeDecodeError
finally:
handle.close() #always runs after try
- else塊。try/except/else結構可以清晰描述哪些異常會由自己的代碼處理,哪些異常會傳播到上一級。通過使用else可以縮減try中的代碼量,并把沒有發生異常時索要執行的語句和try/except代碼塊隔開。
def load_json_key(data, key):
try:
result_dict = json.loads(data) # May raise ValueError
except ValueError as e:
raise KeyError from e
else:
return result_dict[key] # May raise KeyError
- 順利運行try塊后,若想使某些操作能在finally塊的清理代碼之前執行,則可將這些操作寫入else塊中。
```python
try:
# 讀取文件并處理內容
except Exception as e:
# 應對可能發生的異常
else:
# 實時更新文件并把更新中可能出現的異常上報給上級代碼
finally:
#清理代碼