教你写出可读性高的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?=?argsreturn?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ù)可以使用四種不同的方式傳遞給函數(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 知道如何用一個手指殺死對方,但從不會那么去做。
我們都是負(fù)責(zé)任的用戶
如前所述,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ū)傩?#xff0c;應(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??#?拋出一個異常可能會更好if?not?b:return?None??#?拋出一個異常可能會更好#?一些復(fù)雜的代碼試著用?a,b,c?來計算x#?如果成功了,抵制住返回?x?的誘惑if?not?x:#?使用其他的方法來計算出?xreturn?x??#?返回值?x?只有一個出口點有利于維護(hù)代碼習(xí)語(Idiom)
編程習(xí)語,說得簡單些,就是寫代碼的 方式。編程習(xí)語的概念在 c2 和 Stack Overflow 上有詳盡的討論。
符合習(xí)語的 Python 代碼通常被稱為 Pythonic。
通常只有一種、而且最好只有一種明顯的方式去編寫代碼。對 Python 初學(xué)者來說,無意識的情況下很少能寫出習(xí)語式 Python 代碼,所以應(yīng)該有意識地去獲取習(xí)語的書寫方式。
如下有一些常見的 Pythonic:
解包(Unpacking)
如果你知道一個列表或者元組的長度,你可以將其解包并為它的元素取名。比如,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創(chuàng)建一個被忽略的變量
如果你需要賦值(比如,在 解包(Unpacking) )但不需要這個變量,請使用 __:
filename?=?'foobar.txt' basename,?__,?ext?=?filename.rpartition('.')注意
許多 Python 風(fēng)格指南建議使用單下劃線的 _ 而不是這里推薦的雙下劃線 __ 來標(biāo)記廢棄變量。問題是, _常用在作為 gettext() 函數(shù)的別名,也被用在交互式命令行中記錄最后一次操作的值。相反,使用雙下劃線 十分清晰和方便,而且能夠消除使用其他這些用例所帶來的意外干擾的風(fēng)險。
創(chuàng)建一個含 N 個對象的列表
使用 Python 列表中的 * 操作符:
four_nones?=?[None]?*?4創(chuàng)建一個含 N 個列表的列表
因為列表是可變的,所以 * 操作符(如上)將會創(chuàng)建一個包含 N 個且指向 同一個 列表的列表,這可能不是你想用的。取而代之,請使用列表解析:
four_lists?=?[[]?for?__?in?xrange(4)]注意:在 Python 3 中使用 range() 而不是 xrange()。
根據(jù)列表來創(chuàng)建字符串
創(chuàng)建字符串的一個常見習(xí)語是在空的字符串上使用 str.join() :
letters?=?['s',?'p',?'a',?'m'] word?=?''.join(letters)這會將 word 變量賦值為 spam。這個習(xí)語可以用在列表和元組中。
在集合體(collection)中查找一個項
有時我們需要在集合體中查找。讓我們看看這兩個選擇,列表和集合(set),用如下代碼舉個例子:
s?=?set(['s',?'p',?'a',?'m']) l?=?['s',?'p',?'a',?'m']def?lookup_set(s):return?'s'?in?sdef?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)雅的。
行的延續(xù)
當(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)盡管如此,通常情況下,必須去分割一個長邏輯行意味著你同時想做太多的事,這可能影響可讀性。
作者:Python 最佳實踐指南 2018?
來源:
https://learnku.com/docs/python-guide/2018/writing-style/3261
來和小伙伴們一起向上生長呀!
掃描下方二維碼,添加小詹微信,可領(lǐng)取千元大禮包并申請加入 Python 學(xué)習(xí)交流群,群內(nèi)僅供學(xué)術(shù)交流,日常互動,如果是想發(fā)推文、廣告、砍價小程序的敬請繞道!一定記得備注「交流學(xué)習(xí)」,我會盡快通過好友申請哦!
????長按識別,添加微信
(添加人數(shù)較多,請耐心等待)
????長按識別,關(guān)注小詹
(掃碼回復(fù) 1024 領(lǐng)取程序員大禮包)
總結(jié)
以上是生活随笔為你收集整理的教你写出可读性高的Python代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python3.9又更新了:dict内置
- 下一篇: Python 常见的17个错误分析