如何寫一個更好的Python函數?
乾明 編譯整理自 Medium
量子位 報道 | 公眾號 QbitAI
Python雖然好用,但用好真的很難。
尤其是函數部分,只要寫不好,後面的一連串人都會遭殃。
看又看不懂,測試起來也麻煩,維護又維護不動,真是讓人頭疼。
那怎麼寫好一個Python函數呢?
《Writing Idiomatic Python》一書的作者在Medium上發表了一篇文章,給出了6個建議。
希望能夠給你帶來幫助。
什麼樣的函數是一個好函數?
「好」的Python函數和「差」的Python函數之間有什麼差別呢?每個人都有自己的理解。基於我的理解,如果一個Python函數能夠符合下面的大部分條件,我會認為它是一個「好」函數:
命名合理
單一功能
包括文檔字元串
返回一個值
不超過50行
是冪等函數或純函數
對許多人來說,這些要求可能顯得過於苛刻了。
不過,我保證,如果你的函數遵循這些規則,你的代碼會非常漂亮,會讓其他的程序員都「饞哭」的。
下面,我將一一討論這些規則,然後總結它們是如何創造「好」函數的。
命名
在這個問題上,我最喜歡的一句話是:
計算機科學中只有兩件事很讓人頭疼:緩存失效和命名。
儘管這聽起來很莫名其妙,但給一個事情命名太難了。下面是一個反面案例:
defgetknn(from_df):
原文中這個代碼沒有放上去,量子位根據上下文信息進行了補充。
這個函數命名的第一個問題是它使用了縮寫。
對於那些並不出名的縮略詞來說,使用完整的英語單詞會更好。縮寫單詞的唯一原因是為了節省打字時間,但是每個現代編輯器都有自動填充功能,所以你只需要鍵入一次全名就可以了。
縮寫通常是特定領域的。在上面的代碼中,KNN指的是「K-Nearest Neighbors」,df指的是「DataFrame」,這是一個數據結構。如果另一個不熟悉這些首字母縮寫的程序員正在閱讀代碼,幾乎很難看懂。
關於這個函數的名字還有另外兩個小瑕疵:
「get」這個詞是無關緊要的。對於大多數命名比較好的函數來說,很明顯有一些東西會從函數中返回,它的名字將反映這一點。
from_df也不是必要的。如果沒有明確的參數名稱,函數的文檔字元串或類型注釋會描述參數的類型。
那麼我們如何重命名這個函數呢?很簡單:
defk_nearest_neighbors(dataframe):
即使是外行,這個函數要計算的內容也很清楚,參數的名稱(dataframe)也清楚地表明了參數類型。
單一功能
單一功能原則不僅適用於類和模塊,也同樣適用於函數。
一個函數應該只有一個功能。也就是說,它應該只做一件事。
一個重要的原因是,如果每個函數只做一件事,只有這件事發生了變化,才需要改變這個函數。
此外,如果這個函數的單個功能不再需要了,直接把它刪了就行了。
還是用例子來說明吧。下面這個函數,可以做不止一件「事情」:
defcalculate_andprint_stats(list_of_numbers):
sum = sum(list_of_numbers)
mean = statistics.mean(list_of_numbers)
median = statistics.median(list_of_numbers)
mode = statistics.mode(list_of_numbers)
print("-----------------Stats-----------------")
print("SUM: {}".format(sum) print("MEAN: {}".format(mean)
print("MEDIAN: {}".format(median)
print("MODE: {}".format(mode)
這個函數做了兩件事:一是計算一組關於數字列表的統計數據,二是將它們列印到STDOUT。
如果需要計算新的或不同的統計數據,或者需要改變輸出的格式,就需要對這個函數進行調整。
所以,這個函數最好寫成兩個獨立的函數:一個用來執行並返回計算結果,另一個用來獲取這些結果並列印出來。
這種處理方式,不僅能讓測試函數更容易,並且還允許這兩個部分有了遷移性,如果合適的話,還可能一起應用到不同的模塊中。
在編程中,你會發現好多函數都可以做很多很多事情。同樣,為了可讀性和可測試性,這些函數應該被分解成更小的函數,每個函數只有一個功能。
文檔字元串(Docstrings)
雖然每個人似乎都知道PEP - 8,它定義了Python的樣式指南,但是很少有人知道PEP - 257,它是關於文檔字元串的。我再這裡不簡單地重複PEP - 257的內容了,你可以在閑暇時讀一下。其中的關鍵內容是:
每個函數都需要有一個文檔字元串
使用適當的語法和標點符號;用完整的句子寫
首先對函數的作用進行一句話的總結
使用說明性語言而不是描述性語言
在編寫函數時,要養成寫文檔字元串的習慣,並在編寫函數代碼之前嘗試寫一下。
如果你不能寫一個清晰的文檔字元串來描述函數做什麼,就說明你需要再考慮考慮為什麼要寫這個函數了。
返回值
函數可以被認為是一些獨立的程序。它們以參數的形式接受一些輸入,並返回一些結果。
參數有沒有都可以,但從Python內部的角度來看,返回值是必須要有的。你不可能創建一個沒有返回值的函數。如果函數沒有返回值,Python會「強制」返回None。你可以測試一下這段代碼:
python3
Python3.7.0(default, Jul232018,20:22:55)
[Clang9.1.0(clang-902.0.39.2)] on darwin
Type"help","copyright","credits"or"license"formore information.
>>>defadd(a, b):
... print(a + b)
...
>>> b = add(1,2)
3
>>> b
>>> bisNone
True
你會發現 b 的返回值實際上是 None。 即使你寫的函數沒有返回語句,它仍然會返回一些東西。而且,每個函數都應該返回一個有用的值,測試起來也會更方便。畢竟,你寫的代碼應該能夠被測試。
試想一下,測試上面的add函會有多艱難。遵循這個概念,我們應該這樣寫代碼:
withopen("foo.txt","r")asinput_file:
forlineininput_file:
ifline.strip().lower().endswith("cat"):
# ... do something useful with these lines
if line.strip().lower().endswith(『cat』):這一行能夠工作,是因為每個字元串方法( strip ( )、lower ( )、end swith ( ) )都返回一個字元串作為調用函數的結果。
當給定函數沒有返回值時,有一些常見的原因:
「它所做的只是[一些與I / O相關的事情,比如將一個值保存到資料庫中]。我不能返回任何有用的東西。」
我不同意。如果操作順利完成,函數可以返回True。
「我們修改了其中一個參數,將其用作參考參數。」
這裡有兩點需要注意。首先,盡最大努力避免這種做法。用好了令人驚訝,用不好非常危險。
其次,即使這樣做不可行,複製某個參數的成本太高,你也可以回到上一條建議。
「我需要返回多個值。單獨返回一個值是沒有意義的。」
可以使用元組返回多個值。
總是返回一個有用的值,調用者總是可以自由地忽略它們。
函數長度
讓你讀一個200行的函數,並說出它是做什麼的,你是什麼感受?
函數的長度直接影響可讀性,從而影響可維護性。所以要保持你的函數簡短。50行是一個隨意的數字,在我看來是合理的。你編寫的大多數函數應該要短一些。
如果一個函數遵循單一功能原則,它很可能是相當短的。 如果它是純函數或是冪等的(下面討論) ,它也可能是短的。
那麼,如果函數太長,應該怎麼做?重構。這會改變程序的結構而不改變其行為。
從一個長函數中提取幾行代碼,並把它們變成自己的函數。這是縮短長函數的最快、也是最常見的方式。
加上你給所有這些新函數取了合適的名稱,因此生成的代碼讀起來也會更容易。
冪等和函數純度
不管被調用了多少次,冪等函數總是在給定相同參數集的情況下返回相同的值。
結果不依賴於非局部變數、參數的可變性或來自任何I / O流的數據。下面的這個add_three(number)函數是冪等函數:
不管一個人調用add_three(7)多少次,答案總是10。以下是一個非冪等函數:
defadd_three():
"""Return 3 + the number entered by the user."""
number = int(input("Enter a number: "))
returnnumber +3
這個函數的返回值取決於I / O,即用戶輸入的數字。對add_three()的每次調用都會返回不同的值。
如果它被調用兩次,用戶可以第一次輸入3,第二次輸入7,分別調用add_three()返回6和10。
冪等性的一個現實中例子是在電梯前點擊「向上」按鈕。第一次按時,電梯會被「通知」你要上去。因為按按鈕是冪等的,所以反覆按它都沒有什麼影響。結果是一樣的。
為什麼冪等很重要?
可維護性和可維護性。冪等函數很容易測試,因為在使用相同的參數時,它們總是返回相同的結果。
測試僅僅是檢查通過不同調用返回值的預期值。更重要的是,這些測試很快,這是單元測試中一個重要且經常被忽視的問題。
而在處理冪等函數時,重構是輕而易舉的事情。 無論如何在函數之外更改代碼,使用相同的參數調用它的結果總是一樣的。
什麼是純函數?
在函數編程中,如果一個函數既冪等又沒有可觀察到的副作用,它就被認為是純函數。函數外部的任何東西都不會影響這個值。
然而,這並不意味著函數不能影響非局部變數或I / O流之類的事情。例如,如果上面add_three(number)的冪等版本在返回結果之前列印了結果,那麼它仍然被認為是冪等的,因為當它訪問I / O流時,這個訪問與從函數返回的值無關。
調用print ( )只是一個副作用:除了返回值之外,還與程序的其他部分或系統本身進行了一些交互。
讓我們把我們的add_three(number)示例再向前推進一步。我們可以編寫下面的代碼片段來確定調用add_three(number)的次數:
add_three_calls =
defadd_three(number):
"""Return *number* + 3."""
globaladd_three_calls
print(f"Returning")
add_three_calls +=1
returnnumber +3
defnum_calls():
"""Return the number of times *add_three* was called."""
returnadd_three_calls
我們現在正在列印到控制台(一個副作用)並修改一個非局部變數(另一個副作用),但是由於這兩者都不影響函數返回的值,它仍然是冪等的。
純函數沒有副作用。它不僅不使用任何「外部數據」來計算值,除了計算和返回所述值之外,它與系統/程序的其餘部分都沒有交互。因此,雖然我們新的add_three(number)定義仍然是冪等的,但它不再是純的。
純函數沒有日誌語句或print ( )調用。它們不使用資料庫或互聯網連接。它們不訪問或修改非局部變數。它們不調用任何其他非純函數。
簡而言之,它們無法做到愛因斯坦所說的「遠距離幽靈般的行動」(在計算機科學環境中)。它們不會以任何方式修改程序或系統的其餘部分。
在命令式編程(編寫Python代碼時所做的那種)中,它們是所有函數中最安全的函數。
它們也很容易被測試和維護,甚至比只是冪等函數更重要的是,測試它們基本上可以和執行它們一樣快。
測試本身很簡單:沒有資料庫連接或其他外部資源進行模擬,也不需要安裝代碼,之後也沒有什麼需要清理的。
明確地說,冪等性和純函數只是一種期望,不是必需的。也就是說,由於好處很多,我們可能會希望只編寫純函數或冪等函數,但這不現實。
重要的是,我們要有意識開始寫代碼來隔離副作用和外部依賴性。這會使得我們編寫的每一行代碼都更容易被測試。
—完—
加入社群
量子位AI社群開始招募啦,歡迎對AI感興趣的同學,在量子位公眾號(QbitAI)對話界面回復關鍵字「交流群」,獲取入群方式;
此外,量子位專業細分群(自動駕駛、CV、NLP、機器學習等)正在招募,面向正在從事相關領域的工程師及研究人員。
進專業群請在量子位公眾號(QbitAI)對話界面回復關鍵字「專業群」,獲取入群方式。(專業群審核較嚴,敬請諒解)
誠摯招聘
量子位正在招募編輯/記者,工作地點在北京中關村。期待有才氣、有熱情的同學加入我們!相關細節,請在量子位公眾號(QbitAI)對話界面,回復「招聘」兩個字。


TAG:量子位 |