歡迎光臨
每天分享高質量文章

Python程式設計中的反樣式

這篇文章收集了我在Python新手開發者寫的程式碼中所見到的不規範但偶爾又很微妙的問題。

本文的目的是為了幫助那些新手開發者渡過寫出醜陋的Python程式碼的階段。

對於那些新手開發者,總有一些使用反樣式的理由,我已經嘗試在可能的地方給出了這些理由。

但通常這些反樣式會造成程式碼缺乏可讀性、更容易出bug且不符合Python的程式碼風格。

迭代

range的使用

Python程式設計新手喜歡使用range來實現簡單的迭代,在迭代器的長度範圍內來獲取迭代器中的每一個元素:

應該牢記:range並不是為了實現序列簡單的迭代。相比那些用數字定義的for迴圈,雖然用range實現的for迴圈顯得很自然,但是用在序列的迭代上卻容易出bug,而且不如直接構造迭代器看上去清晰:

range的濫用容易造成意外的大小差一(off-by-one)錯誤,這通常是由於程式設計新手忘記了range生成的物件包括range的第一個引數而不包括第二個,類似於java中的substring和其他眾多這種型別的函式。那些認為沒有超出序列結尾的程式設計新手將會製造出bug:

不恰當地使用range的常見理由:
1.需要在迴圈中使用索引。

這並不是一個合理的理由,可以用以下方式代替使用索引:

2.需要同時迭代兩個迴圈,用同一個索引來獲取兩個值。

這種情況下,可以用zip來實現:

3.需要迭代序列的一部分。在這種情況下,僅需要迭代序列切片就可以實現,註意新增必要的註釋註明用意:

有一個例外:

當你迭代一個很大的序列時,切片操作引起的開銷就比較大。

如果序列只有10個元素,就沒有什麼問題;但是如果有1000萬個元素時,或者在一個效能敏感的內迴圈中進行切片操作時,開銷就變得非常重要了。

這種情況下可以考慮使用xrange代替range [1]。

在用來迭代序列之外,range的一個重要用法是當你真正想要生成一個數字序列而不是用來生成索引:

正確使用串列解析

如果你有像這樣的一個迴圈:

你可以使用串列解析來重寫:

為什麼要這麼做?

一方面你避免了正確初始化串列可能帶來的錯誤,另一方面,這樣寫程式碼讓看起來很乾凈,整潔。

對於那些有函式式程式設計背景的人來說,使用map函式可能感覺更熟悉,但是在我看來這種做法不太Python化。

其他的一些不使用串列解析的常見理由:

1. 需要迴圈巢狀。

這個時候你可以巢狀整個串列解析,或者在串列解析中多行使用迴圈:

使用串列解析:

註意:在有多個迴圈的串列解析中,迴圈有同樣的順序就像你並沒有使用串列解析一樣。

2. 你在迴圈內部需要一個條件判斷。

你只需要把這個條件判斷新增到串列解析中去:

一個不使用串列解析的合理的理由是你在串列解析裡不能使用異常處理。

如果迭代中一些元素可能引起異常,你需要在串列解析中透過函式來電轉駁可能的異常處理,或者乾脆不使用串列解析。

效能缺陷

線上性時間內檢查內容

在語法上,檢查list或者set/dict中是否包含某個元素錶面上看起來沒什麼區別,但是錶面之下卻是截然不同的。

如果你需要重覆檢查某個資料結構裡是否包含某個元素,最好使用set來代替list。(如果你想把一個值和要檢查的元素聯絡起來,可以使用dict;這樣同樣可以實現常數檢查時間。)

Python中set的元素和dict的鍵值是可雜湊的,因此查詢起來時間複雜度為O(1)。

應該記住:

建立set引入的是一次性開銷,建立過程將花費線性時間即使成員檢查花費常數時間。

因此如果你需要在迴圈裡檢查成員,最好先花時間建立set,因為你只需要建立一次。

變數洩露

迴圈

通常說來,在Python中,一個變數的作用域比你在其他語言裡期望的要寬。

例如:在Java中下麵的程式碼將不能透過編譯:

然而在Python中,同樣的程式碼總會順利執行且得到意料中的結果:

這段程式碼將會正常執行,除非子y為空的情況下,此時,迴圈永遠不會執行,而且processList函式的呼叫將會丟擲NameError異常,因為idx沒有定義。

如果你使用Pylint程式碼檢查工具,將會警告:使用可能沒有定義的變數idx。

解決辦法永遠是顯然的,可以在迴圈之前設定idx為一些特殊的值,這樣你就知道如果迴圈永遠沒有執行的時候你將要尋找什麼。

這種樣式叫做哨兵樣式。那麼什麼值可以用來作為哨兵呢?

在C語言時代或者更早,當int統治程式設計世界的時候,對於需要傳回一個期望的錯誤結果的函式來說為通用的樣式為傳回-1。

例如,當你想要傳回串列中某一元素的索引值:

通常情況下,在Python裡None是一個比較好的哨兵值,即使它不是一貫地被Python標準型別使用(例如:str.find [2])

外作用域

Python程式員新手經常喜歡把所有東西放到所謂的外作用域——python檔案中不被程式碼塊(例如函式或者類)包含的部分。

外作用域相當於全域性名稱空間;為了這部分的討論,你應該假設全域性作用域的內容在單個Python檔案的任何地方都是可以訪問的。

對於定義整個模組都需要去訪問的在檔案頂部宣告的常量,外作用域顯得非常強大。

給外作用域中的任何變數使用有特色的名字是明智的做法,例如,使用IN_ALL_CAPS 這個常量名。 這將不容易造成如下bug:

如果你看的近一點,你將看到print_file函式的定義中用filenam命名引數名,但是函式體卻取用的卻是filename。

然而,這個程式仍然可以執行得很好。

為什麼呢?

在print_file函式裡,當一個區域性變數filename沒有被找到時,下一步是在全域性作用域中去尋找。

由於print_file的呼叫在外作用域中(即使有縮排),這裡宣告的filename對於print_file函式是可見的。

那麼如何避免這樣的錯誤呢?

首先,在外作用域中不是IN_ALL_CAPS這樣的全域性變數就不要設定任何值[3]。

引數解析最好交給main函式,因此函式中任何內部變數不在外作用域中存活。

這也提醒人們關註全域性關鍵字global。如果你只是讀取全域性變數的值,你就不需要全域性關鍵字global。

你只有在想要改變全域性變數名取用的物件時有使用global關鍵字的必要。

程式碼風格

向PEP8致敬

PEP 8是Python程式碼的通用風格指南,你應該牢記在心並且盡可能去遵循它,儘管一些人有充分的理由不同意其中一些細小的風格,例如縮排的空格個數或使用空行。

如果你不遵循PEP8,你應該有除“我只是不喜歡那樣的風格”之外更好的理由。下邊的風格指南都是從PEP8中摘取的,似乎是程式設計者經常需要牢記的。

測試是否為空

如果你要檢查一個容器型別(例如:串列,詞典,集合)是否為空,只需要簡單測試它而不是使用類似檢查len(x)>0這樣的方法:

如果你想在其他地方儲存positive_numbers是否為空的結果,可以使用bool(positive_number)作為結果儲存;bool用來判斷if條件判斷陳述句的真值。

測試是否為None

如前面所提到,None可以作為一個很好的哨兵值。那麼如何檢查它呢?

如果你明確的想要測試None,而不只是測試其他一些值為False的項(如空容器或者0),可以使用:

如果你使用None作為哨兵,這也是Python風格所期望的樣式,例如在你想要區分None和0的時候。

如果你只是測試變數是否為一些有用的值,一個簡單的if樣式通常就夠用了:

例如:如果期望x是一個容器型別,但是x可能作另一個函式的傳回結果值變為None,你應該立即考慮到這種情況。你需要留意是否改變了傳給x的值,否則可能你認為True或0. 0是個有用的值,程式卻不會按照你想要的方式執行。

《Linux雲端計算及運維架構師高薪實戰班》2018年09月17日即將開課中,120天衝擊Linux運維年薪30萬,改變速約~~~~

    *宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

    – END –


    更多Linux好文請點選【閱讀原文】

    ↓↓↓

    贊(0)

    分享創造快樂