如果有人問起 Python 程序員他們最喜歡 Python 哪一點,他們一定會提到 Python 的高可讀性。確實,對于 Python 來說,其高可讀性一直是 Python 這門語言設(shè)計的核心。一個不爭的事實是,相對于寫代碼而言,讀代碼才是更加平常的事情。
Python 代碼有高可讀性的一個原因就是其有著相對而言更加完善的編碼風(fēng)格準(zhǔn)則和 「Python 化」習(xí)慣語法。
當(dāng) Python 老手(Pythonista)認(rèn)為一段代碼不「Python 化」,他們通常的意思是這段代碼沒有遵循一般準(zhǔn)則,同時亦沒有以最佳的(最具可讀性的)方式表達(dá)出代碼的意圖。
在一些極端的情況下,沒有公認(rèn)最佳的方式來表達(dá) Python 代碼的意圖,不過這種極端情況非常罕見。
盡管 Python 可以寫出從各種意義上來說都像是黑魔法的代碼,但最簡單直白的表達(dá)才是正道。
不優(yōu)雅
def make_complex(*args):
x, y = args
return dict(**locals())
優(yōu)雅
def make_complex(x, y):
return {'x': x, 'y': y}
在上述好的代碼中,x 和 y 清晰明了的從參數(shù)中獲取值,并清晰明了的返回了一個字典。當(dāng)開發(fā)者看到這個函數(shù)后就可以明了這個函數(shù)的用途,而不好的代碼則不行。
雖然在 Python 中我們推崇使用形如列表生成式這種簡潔明了的復(fù)合語句,但是除此以外,我們應(yīng)該盡量避免將兩句獨立分割的代碼寫在同一行。
不好的風(fēng)格
print 'one'; print 'two'
if x == 1: print 'one'
if <complex comparison> and <other complex comparison>:
# do something
好的風(fēng)格
print 'one'
print 'two'
if x == 1:
print 'one'
cond1 = <complex comparison>
cond2 = <other complex comparison>
if cond1 and cond2:
# do something
函數(shù)的參數(shù)可以使用四種不同的方式傳遞給函數(shù)。
必選參數(shù) 是沒有默認(rèn)值的必填的參數(shù)。必選參數(shù)是最簡單的參數(shù)構(gòu)成,用于參數(shù)較少的函數(shù)的構(gòu)成,是該函數(shù)意義的一部分,使用他們的順序是按照定義自然排序的。舉個例子,對于 send(message, recipient)
和 point(x, y)
這兩個函數(shù),使用函數(shù)的人需要知道這個函數(shù)需要兩個參數(shù),并且記住兩個參數(shù)的順序。
在調(diào)用函數(shù)的時候,我們也可以使用參數(shù)的名稱調(diào)用。使用參數(shù)的名稱的方式可以調(diào)換參數(shù)的順序,就像 send(recipient='World',message='Hello')
和 point(y=2, x=1)
這樣。但這樣的做法會降低代碼的可讀性,并且使代碼冗長,因此更建議使用 send('Hello', 'World')
和 point(1,2)
這樣的方式調(diào)用。
關(guān)鍵字參數(shù) 是非強制的,且有默認(rèn)值。它們經(jīng)常被用在傳遞給函數(shù)的可選參數(shù)中。當(dāng)一個函數(shù)有超過兩個或三個位置參數(shù)時,函數(shù)簽名會變得難以記憶,使用帶有默認(rèn)參數(shù)的關(guān)鍵字參數(shù)有時候會給你帶來便利。比如,一個更完整的 send
函數(shù)可以被定義為 send(message, to, cc=None, bcc=None)
。這里的 cc
和 bcc
是可選的, 當(dāng)沒有傳遞給它們其他值的時候,它們的值就是 None。
Python 中有多種方式調(diào)用帶關(guān)鍵字參數(shù)的函數(shù)。比如說,我們可以按照定義時的參數(shù)順序而無需明確的命名參數(shù)來調(diào)用函數(shù),就像 send('Hello', 'World', 'Cthulhu', 'God')
是將密件發(fā)送給上帝。我們也可以使用命名參數(shù)而無需遵循參數(shù)順序來調(diào)用函數(shù),就像 send('Hello again', 'World', bcc='God', cc='Cthulhu')
。沒有特殊情況的話,這兩種方式都需要盡力避免,最優(yōu)的調(diào)用方式是與定義方式一致:send('Hello', 'World', cc='Cthulhu',bcc='God')
。
作為附注,請遵循 YAGNI 原則。通常,移除一個用作『以防萬一』但從未使用的可選參數(shù)(以及它在函數(shù)中的邏輯),比添加一個所需的新的可選參數(shù)和它的邏輯要來的困難。
任意參數(shù)列表 是第三種給函數(shù)傳參的方式。如果函數(shù)的參數(shù)數(shù)量是動態(tài)的,該函數(shù)可以被定義成 *args
的結(jié)構(gòu)。在這個函數(shù)體中, args
是一個元組,它包含所有剩余的位置參數(shù)。舉個例子, 我們可以用任何容器作為參數(shù)去調(diào)用 send(message, *args)
,比如 send('Hello', 'God', 'Mom','Cthulhu')
。在此函數(shù)體中, args
相當(dāng)于 ('God','Mom', 'Cthulhu')
。
然而,這種結(jié)構(gòu)有一些缺點,使用時應(yīng)該特別注意。如果一個函數(shù)接受的參數(shù)列表具有相同的性質(zhì),通常把它定義成一個參數(shù),這個參數(shù)是一個列表或者其他任何序列會更清晰。在這里,如果 send
參數(shù)有多個容器(recipients),將之定義成 send(message,recipients)
會更明確,調(diào)用它時就使用 send('Hello', ['God', 'Mom', 'Cthulhu'])
。這樣的話, 函數(shù)的使用者可以事先將容器列表維護(hù)成列表(list)形式,這為傳遞各種不能被轉(zhuǎn)變成其他序列的序列(包括迭代器)帶來了可能。
任意關(guān)鍵字參數(shù)字典 是最后一種給函數(shù)傳參的方式。如果函數(shù)要求一系列待定的命名參數(shù),我們可以使用 **kwargs
的結(jié)構(gòu)。在函數(shù)體中, kwargs
是一個字典,它包含所有傳遞給函數(shù)但沒有被其他關(guān)鍵字參數(shù)捕捉的命名參數(shù)。
和 任意參數(shù)列表 中所需注意的一樣,相似的原因是:這些強大的技術(shù)在非特殊情況下,都要盡量避免使用,因為其缺乏簡單和明確的結(jié)構(gòu)來足夠表達(dá)函數(shù)意圖。
編寫函數(shù)的時候采用何種參數(shù)形式,是用位置參數(shù),還是可選關(guān)鍵字參數(shù),是否使用形如任意參數(shù) 的高級技術(shù),這些都由程序員自己決定。如果能明智地遵循上述建議,即可輕松寫出這樣的 Python 函數(shù):
易讀(名字和參數(shù)無需解釋)
易改(添加新的關(guān)鍵字參數(shù)不會破壞代碼的其他部分)
Python 對駭客來說是一個強有力的工具,它擁有非常豐富的鉤子(hook)和工具,允許你施展幾乎任何形式的技巧。比如說,它能夠做以下:
改變對象創(chuàng)建和實例化的方式;
改變 Python 解釋器導(dǎo)入模塊的方式;
甚至可能(如果需要的話也是被推薦的)在 Python 中嵌入 C 程序。
盡管如此,所有的這些選擇都有許多缺點。使用最直接的方式來達(dá)成目標(biāo)通常是最好的方法。它們最主要的缺點是可讀性不高。許多代碼分析工具,比如說 pylint 或者 pyflakes,將無法解析這種『魔法』代碼。
我們認(rèn)為 Python 開發(fā)者應(yīng)該知道這些近乎無限的可能性,因為它為我們灌輸了沒有不可能完成的任務(wù)的信心。然而,知道何時 不能 使用它們也是非常重要的。
就像一位功夫大師,一個 Pythonista 知道如何用一個手指殺死對方,但從不會那么去做。
如前所述,Python 允許很多技巧,其中一些具有潛在的危險。一個好的例子是:任何客戶端代碼能夠重寫一個對象的屬性和方法(Python 中沒有 private
關(guān)鍵字)。這種哲學(xué)是在說:『我們都是負(fù)責(zé)任的用戶』,它和高度防御性的語言(如 Java,擁有很多機制來預(yù)防錯誤操作)有著非常大的不同。
這并不意味著,比如說,Python 中沒有屬性是私有的,也不意味著沒有合適的封裝方法。與其依賴在開發(fā)者的代碼之間樹立起的一道道隔墻,Python 社區(qū)更愿意依靠一組約定,來表明這些元素不應(yīng)該被直接訪問。
私有屬性的主要約定和實現(xiàn)細(xì)節(jié)是在所有的 內(nèi)部 變量前加一個下劃線。如果客戶端代碼打破了這條規(guī)則并訪問了帶有下劃線的變量,那么因內(nèi)部代碼的改變而出現(xiàn)的任何不當(dāng)?shù)男袨榛騿栴},都是客戶端代碼的責(zé)任。
鼓勵大方地使用此約定:任何不開放給客戶端代碼使用的方法或?qū)傩?,?yīng)該有一個下劃線前綴。這將保證更好的職責(zé)劃分以及更容易對已有代碼進(jìn)行修改。將一個私有屬性公開化總是可能的,但是把一個公共屬性私有化可能是一個更難的選擇。
當(dāng)一個函數(shù)變得復(fù)雜,在函數(shù)體中使用多返回值的語句并不少見。然而,為了保持函數(shù)的可讀性,建議在函數(shù)體中避免使用返回多個有意義的值。
在函數(shù)中返回結(jié)果主要有兩種情況:函數(shù)正常運行并返回它的結(jié)果,以及錯誤的情況,要么因為一個錯誤的輸入?yún)?shù),要么因為其他導(dǎo)致函數(shù)無法完成計算或任務(wù)的原因。
如果你在面對第二種情況時不想拋出異常,返回一個值(比如說 None 或 False )來表明函數(shù)無法正確運行,可能是需要的。在這種情況下,越早返回所發(fā)現(xiàn)的不正確上下文越好。這將幫助扁平化函數(shù)的結(jié)構(gòu):我們假定在『因為錯誤而返回』的語句后的所有代碼都能夠滿足函數(shù)主要結(jié)果運算。這種類型的多發(fā)揮結(jié)果,是有必要的。
然而,當(dāng)一個函數(shù)在其正常運行過程中有多個主要出口點時,它會變得難以調(diào)試其返回結(jié)果,所以保持單個出口點可能會更好。這也將有助于提取某些代碼路徑,而且多個出口點很有可能意味著這里需要重構(gòu):
def complex_function(a, b, c):
if not a:
return None # 拋出一個異??赡軙?br> if not b:
return None # 拋出一個異常可能會更好
# 一些復(fù)雜的代碼試著用 a,b,c 來計算x
# 如果成功了,抵制住返回 x 的誘惑
if not x:
# 使用其他的方法來計算出 x
return x # 返回值 x 只有一個出口點有利于維護(hù)代碼
編程習(xí)語,說得簡單些,就是寫代碼的 方式。編程習(xí)語的概念在 c2 和 Stack Overflow 上有詳盡的討論。
符合習(xí)語的 Python 代碼通常被稱為 Pythonic。
通常只有一種、而且最好只有一種明顯的方式去編寫代碼。對 Python 初學(xué)者來說,無意識的情況下很少能寫出習(xí)語式 Python 代碼,所以應(yīng)該有意識地去獲取習(xí)語的書寫方式。
如下有一些常見的 Pythonic:
如果你知道一個列表或者元組的長度,你可以將其解包并為它的元素取名。比如,enumerate()
會對 list 中的每個項提供包含兩個元素的元組:
for index, item in enumerate(some_list):
# do something with index and item
你也能通過這種方式交換變量:
a, b = b, a
嵌套解包也能工作:
a, (b, c) = 1, (2, 3)
Python 3 提供了擴展解包的新方法在 PEP 3132 有介紹:
a, *rest = [1, 2, 3]
# a = 1, rest = [2, 3]
a, *middle, c = [1, 2, 3, 4]
# a = 1, middle = [2, 3], c = 4
如果你需要賦值(比如,在 解包(Unpacking) )但不需要這個變量,請使用 __
:
filename = 'foobar.txt'
basename, __, ext = filename.rpartition('.')
注意
許多 Python 風(fēng)格指南建議使用單下劃線的
_
而不是這里推薦的雙下劃線__
來標(biāo)記廢棄變量。問題是,_
常用在作為gettext()
函數(shù)的別名,也被用在交互式命令行中記錄最后一次操作的值。相反,使用雙下劃線 十分清晰和方便,而且能夠消除使用其他這些用例所帶來的意外干擾的風(fēng)險。
使用 Python 列表中的 *
操作符:
four_nones = [None] * 4
因為列表是可變的,所以 *
操作符(如上)將會創(chuàng)建一個包含 N 個且指向 同一個 列表的列表,這可能不是你想用的。取而代之,請使用列表解析:
four_lists = [[] for __ in xrange(4)]
注意:在 Python 3 中使用 range()
而不是 xrange()
。
創(chuàng)建字符串的一個常見習(xí)語是在空的字符串上使用 str.join()
:
letters = ['s', 'p', 'a', 'm']
word = ''.join(letters)
這會將 word 變量賦值為 spam
。這個習(xí)語可以用在列表和元組中。
有時我們需要在集合體中查找。讓我們看看這兩個選擇,列表和集合(set),用如下代碼舉個例子:
s = set(['s', 'p', 'a', 'm'])
l = ['s', 'p', 'a', 'm']
def lookup_set(s):
return 's' in s
def lookup_list(l):
return 's' in l
即使兩個函數(shù)看起來完全一樣,但因為 查找集合 是利用了 Python 中的『集合是可哈?!坏奶匦?,兩者的查詢性能是非常不同的。為了判斷一個項是否在列表中,Python 將會查看每個項直到它找到匹配的項。這是耗時的任務(wù),尤其是對長列表而言。另一方面,在集合中, 項的哈希值將會告訴 Python 在集合的哪里去查找匹配的項。結(jié)果是,即使集合很大,查詢的速度也很快。在字典中查詢也是同樣的原理。想了解更多內(nèi)容,請見 StackOverflow 。想了解在每種數(shù)據(jù)結(jié)構(gòu)上的多種常見操作的花費時間的詳細(xì)內(nèi)容, 請見 此頁面。
因為這些性能上的差異,在下列場景中,使用集合或者字典而不是列表,通常會是個好主意:
集合體中包含大量的項;
你將在集合體中重復(fù)地查找項;
你沒有重復(fù)的項。
對于小的集合體、或者你不會頻繁查找的集合體,建立哈希帶來的額外時間和內(nèi)存的開銷經(jīng)常會大過改進(jìn)搜索速度所節(jié)省的時間。
這里有一些你應(yīng)該遵循的約定,以讓你的代碼更加易讀。
你不需要明確地比較一個值是 True,或者 None,或者 0 - 你可以僅僅把它放在 if
語句中。參閱 真值測試 來了解什么被認(rèn)為是 false:
糟糕:
if attr == True:
print 'True!'
if attr == None:
print 'attr is None!'
優(yōu)雅:
# 檢查值
if attr:
print 'attr is truthy!'
# 或者做相反的檢查
if not attr:
print 'attr is falsey!'
# 或者,None 等于 false,你可以直接相較它進(jìn)行匹配
if attr is None:
print 'attr is None!'
不要使用 dict.has_key()
方法。相反,使用 x in d
語法,或者將默認(rèn)參數(shù)傳遞給 dict.get()
方法。
壞的示例:
d = {'hello': 'world'}
if d.has_key('hello'):
print d['hello'] # prints 'world'
else:
print 'default_value'
推薦的示例:
d = {'hello': 'world'}
print d.get('hello', 'default_value') # prints 'world'
print d.get('thingy', 'default_value') # prints 'default_value'
# 或者:
if 'hello' in d:
print d['hello']
列表推導(dǎo)式 提供了一個強大并且簡潔的方法來對列表價進(jìn)行操作。除此之外,map()
和 filter()
函數(shù)在列表的操作上也是非常簡潔的。
壞:
# Filter elements greater than 4
a = [3, 4, 5]
b = []
for i in a:
if i > 4:
b.append(i)
好:
a = [3, 4, 5]
b = [i for i in a if i > 4]
# Or:
b = filter(lambda x: x > 4, a)
壞:
# Add three to all list members.
a = [3, 4, 5]
for i in range(len(a)):
a[i] += 3
好:
a = [3, 4, 5]
a = [i + 3 for i in a]
# Or:
a = map(lambda i: i + 3, a)
使用 enumerate()
來跟蹤正在被處理的元素索引。
a = [3, 4, 5]
for i, item in enumerate(a):
print i, item
# prints
# 0 3
# 1 4
# 2 5
比起手動計數(shù),使用 enumerate()
函數(shù)有更好的可讀性,而且,他更加適合在迭代器中使用。
使用 with open
語法來讀文件,它能夠為你自動關(guān)閉文件。
壞:
f = open('file.txt')
a = f.read()
print a
f.close()
好:
with open('file.txt') as f:
for line in f:
print line
即使在 with
控制塊中出現(xiàn)了異常,它也能確保你關(guān)閉了文件,因此,使用 with
語法是更加優(yōu)雅的。
當(dāng)一個代碼邏輯行的長度超過可接受的限度時,你需要將之分為多個物理行。如果行的結(jié)尾是一個反斜杠,Python 解釋器會把這些連續(xù)行拼接在一起。這在某些情況下很有幫助, 但我們總是應(yīng)該避免使用,因為它的脆弱性:如果在行的結(jié)尾,在反斜杠后加了空格,這會破壞代碼,而且可能有意想不到的結(jié)果。
一個更好的解決方案是在元素周圍使用括號。左邊以一個未閉合的括號開頭,Python 解釋器會把行的結(jié)尾和下一行連接起來直到遇到閉合的括號。同樣的行為適用中括號和大括號。
糟糕:
my_very_big_string = '''For a long time I used to go to bed early. Sometimes,\
when I had put out my candle, my eyes would close so quickly that I had not even\
time to say 'I'm going to sleep.''''
from some.deep.module.inside.a.module import a_nice_function, another_nice_function,\
yet_another_nice_function
優(yōu)雅:
my_very_big_string = (
'For a long time I used to go to bed early. Sometimes, '
'when I had put out my candle, my eyes would close so quickly '
'that I had not even time to say 'I'm going to sleep.''
)
from some.deep.module.inside.a.module import (
a_nice_function, another_nice_function, yet_another_nice_function)
盡管如此,通常情況下,必須去分割一個長邏輯行意味著你同時想做太多的事,這可能影響可讀性。