函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

Python雖然好用,但用好真的很難。

尤其是函數(shù)部分,只要寫不好,后面的一連串人都會遭殃。

看又看不懂,測試起來也麻煩,維護又維護不動,真是讓人頭疼。

那怎么寫好一個Python函數(shù)呢?

《Writing Idiomatic Python》一書的作者在Medium上發(fā)表了一篇文章,給出了6個建議。

希望能夠給你帶來幫助。

私信回復(fù)【基礎(chǔ)】可獲取python基礎(chǔ)視頻教程一套

什么樣的函數(shù)是一個好函數(shù)?

“好”的Python函數(shù)和“差”的Python函數(shù)之間有什么差別呢?每個人都有自己的理解?;谖业睦斫猓绻粋€Python函數(shù)能夠符合下面的大部分條件,我會認為它是一個“好”函數(shù):

  • 命名合理
  • 單一功能
  • 包括文檔字符串
  • 返回一個值
  • 不超過50行
  • 是冪等函數(shù)或純函數(shù)

對許多人來說,這些要求可能顯得過于苛刻了。

不過,我保證,如果你的函數(shù)遵循這些規(guī)則,你的代碼會非常漂亮,會讓其他的程序員都“饞哭”的。

下面,我將一一討論這些規(guī)則,然后總結(jié)它們是如何創(chuàng)造“好”函數(shù)的。

命名

在這個問題上,我最喜歡的一句話是:

計算機科學中只有兩件事很讓人頭疼:緩存失效和命名。

盡管這聽起來很莫名其妙,但給一個事情命名太難了。下面是一個反面案例:

def get knn(from_df):

原文中這個代碼沒有放上去,量子位根據(jù)上下文信息進行了補充。

這個函數(shù)命名的第一個問題是它使用了縮寫。

對于那些并不出名的縮略詞來說,使用完整的英語單詞會更好??s寫單詞的唯一原因是為了節(jié)省打字時間,但是每個現(xiàn)代編輯器都有自動填充功能,所以你只需要鍵入一次全名就可以了。

縮寫通常是特定領(lǐng)域的。在上面的代碼中,KNN指的是“K-Nearest Neighbors”,df指的是“DataFrame”,這是一個數(shù)據(jù)結(jié)構(gòu)。如果另一個不熟悉這些首字母縮寫的程序員正在閱讀代碼,幾乎很難看懂。

關(guān)于這個函數(shù)的名字還有另外兩個小瑕疵:

  • “get”這個詞是無關(guān)緊要的。對于大多數(shù)命名比較好的函數(shù)來說,很明顯有一些東西會從函數(shù)中返回,它的名字將反映這一點。
  • from_df也不是必要的。如果沒有明確的參數(shù)名稱,函數(shù)的文檔字符串或類型注釋會描述參數(shù)的類型。

那么我們?nèi)绾沃孛@個函數(shù)呢?很簡單:

def k_nearest_neighbors(dataframe):

即使是外行,這個函數(shù)要計算的內(nèi)容也很清楚,參數(shù)的名稱(dataframe)也清楚地表明了參數(shù)類型。

單一功能

單一功能原則不僅適用于類和模塊,也同樣適用于函數(shù)。

一個函數(shù)應(yīng)該只有一個功能。也就是說,它應(yīng)該只做一件事。

一個重要的原因是,如果每個函數(shù)只做一件事,只有這件事發(fā)生了變化,才需要改變這個函數(shù)。

此外,如果這個函數(shù)的單個功能不再需要了,直接把它刪了就行了。

還是用例子來說明吧。下面這個函數(shù),可以做不止一件“事情”:

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

這個函數(shù)做了兩件事:一是計算一組關(guān)于數(shù)字列表的統(tǒng)計數(shù)據(jù),二是將它們打印到STDOUT。

如果需要計算新的或不同的統(tǒng)計數(shù)據(jù),或者需要改變輸出的格式,就需要對這個函數(shù)進行調(diào)整。

所以,這個函數(shù)最好寫成兩個獨立的函數(shù):一個用來執(zhí)行并返回計算結(jié)果,另一個用來獲取這些結(jié)果并打印出來。

這種處理方式,不僅能讓測試函數(shù)更容易,并且還允許這兩個部分有了遷移性,如果合適的話,還可能一起應(yīng)用到不同的模塊中。

在編程中,你會發(fā)現(xiàn)好多函數(shù)都可以做很多很多事情。同樣,為了可讀性和可測試性,這些函數(shù)應(yīng)該被分解成更小的函數(shù),每個函數(shù)只有一個功能。

文檔字符串(Docstrings)

雖然每個人似乎都知道PEP - 8,它定義了Python的樣式指南,但是很少有人知道PEP - 257,它是關(guān)于文檔字符串的。我再這里不簡單地重復(fù)PEP - 257的內(nèi)容了,你可以在閑暇時讀一下。其中的關(guān)鍵內(nèi)容是:

  • 每個函數(shù)都需要有一個文檔字符串
  • 使用適當?shù)恼Z法和標點符號;用完整的句子寫
  • 首先對函數(shù)的作用進行一句話的總結(jié)
  • 使用說明性語言而不是描述性語言

在編寫函數(shù)時,要養(yǎng)成寫文檔字符串的習慣,并在編寫函數(shù)代碼之前嘗試寫一下。

如果你不能寫一個清晰的文檔字符串來描述函數(shù)做什么,就說明你需要再考慮考慮為什么要寫這個函數(shù)了。

返回值

函數(shù)可以被認為是一些獨立的程序。它們以參數(shù)的形式接受一些輸入,并返回一些結(jié)果。

參數(shù)有沒有都可以,但從Python內(nèi)部的角度來看,返回值是必須要有的。你不可能創(chuàng)建一個沒有返回值的函數(shù)。如果函數(shù)沒有返回值,Python會“強制”返回None。你可以測試一下這段代碼:

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

你會發(fā)現(xiàn) b 的返回值實際上是 None。 即使你寫的函數(shù)沒有返回語句,它仍然會返回一些東西。而且,每個函數(shù)都應(yīng)該返回一個有用的值,測試起來也會更方便。畢竟,你寫的代碼應(yīng)該能夠被測試。

試想一下,測試上面的add函會有多艱難。遵循這個概念,我們應(yīng)該這樣寫代碼:

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

if line.strip().lower().endswith(‘cat’):這一行能夠工作,是因為每個字符串方法( strip ( )、lower ( )、end swith ( ) )都返回一個字符串作為調(diào)用函數(shù)的結(jié)果。

當給定函數(shù)沒有返回值時,有一些常見的原因:

“它所做的只是[一些與I / O相關(guān)的事情,比如將一個值保存到數(shù)據(jù)庫中]。我不能返回任何有用的東西?!?/strong>

我不同意。如果操作順利完成,函數(shù)可以返回True。

“我們修改了其中一個參數(shù),將其用作參考參數(shù)?!?/strong>

這里有兩點需要注意。首先,盡最大努力避免這種做法。用好了令人驚訝,用不好非常危險。

其次,即使這樣做不可行,復(fù)制某個參數(shù)的成本太高,你也可以回到上一條建議。

“我需要返回多個值。單獨返回一個值是沒有意義的。”

可以使用元組返回多個值。

總是返回一個有用的值,調(diào)用者總是可以自由地忽略它們。

函數(shù)長度

讓你讀一個200行的函數(shù),并說出它是做什么的,你是什么感受?

函數(shù)的長度直接影響可讀性,從而影響可維護性。所以要保持你的函數(shù)簡短。50行是一個隨意的數(shù)字,在我看來是合理的。你編寫的大多數(shù)函數(shù)應(yīng)該要短一些。

如果一個函數(shù)遵循單一功能原則,它很可能是相當短的。 如果它是純函數(shù)或是冪等的(下面討論) ,它也可能是短的。

那么,如果函數(shù)太長,應(yīng)該怎么做?重構(gòu)。這會改變程序的結(jié)構(gòu)而不改變其行為。

從一個長函數(shù)中提取幾行代碼,并把它們變成自己的函數(shù)。這是縮短長函數(shù)的最快、也是最常見的方式。

加上你給所有這些新函數(shù)取了合適的名稱,因此生成的代碼讀起來也會更容易。

冪等和函數(shù)純度

不管被調(diào)用了多少次,冪等函數(shù)總是在給定相同參數(shù)集的情況下返回相同的值。

結(jié)果不依賴于非局部變量、參數(shù)的可變性或來自任何I / O流的數(shù)據(jù)。下面的這個add_three(number)函數(shù)是冪等函數(shù):

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

不管一個人調(diào)用add_three(7)多少次,答案總是10。以下是一個非冪等函數(shù):

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

這個函數(shù)的返回值取決于I / O,即用戶輸入的數(shù)字。對add_three()的每次調(diào)用都會返回不同的值。

如果它被調(diào)用兩次,用戶可以第一次輸入3,第二次輸入7,分別調(diào)用add_three()返回6和10。

冪等性的一個現(xiàn)實中例子是在電梯前點擊“向上”按鈕。第一次按時,電梯會被“通知”你要上去。因為按按鈕是冪等的,所以反復(fù)按它都沒有什么影響。結(jié)果是一樣的。

為什么冪等很重要?

可維護性和可維護性。冪等函數(shù)很容易測試,因為在使用相同的參數(shù)時,它們總是返回相同的結(jié)果。

測試僅僅是檢查通過不同調(diào)用返回值的預(yù)期值。更重要的是,這些測試很快,這是單元測試中一個重要且經(jīng)常被忽視的問題。

而在處理冪等函數(shù)時,重構(gòu)是輕而易舉的事情。 無論如何在函數(shù)之外更改代碼,使用相同的參數(shù)調(diào)用它的結(jié)果總是一樣的。

什么是純函數(shù)?

在函數(shù)編程中,如果一個函數(shù)既冪等又沒有可觀察到的副作用,它就被認為是純函數(shù)。函數(shù)外部的任何東西都不會影響這個值。

然而,這并不意味著函數(shù)不能影響非局部變量或I / O流之類的事情。例如,如果上面add_three(number)的冪等版本在返回結(jié)果之前打印了結(jié)果,那么它仍然被認為是冪等的,因為當它訪問I / O流時,這個訪問與從函數(shù)返回的值無關(guān)。

調(diào)用print ( )只是一個副作用:除了返回值之外,還與程序的其他部分或系統(tǒng)本身進行了一些交互。

讓我們把我們的add_three(number)示例再向前推進一步。我們可以編寫下面的代碼片段來確定調(diào)用add_three(number)的次數(shù):

函數(shù)寫不好后面的人都會遭殃,如何寫一個更好的Python函數(shù)?

我們現(xiàn)在正在打印到控制臺(一個副作用)并修改一個非局部變量(另一個副作用),但是由于這兩者都不影響函數(shù)返回的值,它仍然是冪等的。

純函數(shù)沒有副作用。它不僅不使用任何“外部數(shù)據(jù)”來計算值,除了計算和返回所述值之外,它與系統(tǒng)/程序的其余部分都沒有交互。因此,雖然我們新的add_three(number)定義仍然是冪等的,但它不再是純的。

純函數(shù)沒有日志語句或print ( )調(diào)用。它們不使用數(shù)據(jù)庫或互聯(lián)網(wǎng)連接。它們不訪問或修改非局部變量。它們不調(diào)用任何其他非純函數(shù)。

簡而言之,它們無法做到愛因斯坦所說的“遠距離幽靈般的行動”(在計算機科學環(huán)境中)。它們不會以任何方式修改程序或系統(tǒng)的其余部分。

在命令式編程(編寫Python代碼時所做的那種)中,它們是所有函數(shù)中最安全的函數(shù)。

它們也很容易被測試和維護,甚至比只是冪等函數(shù)更重要的是,測試它們基本上可以和執(zhí)行它們一樣快。

測試本身很簡單:沒有數(shù)據(jù)庫連接或其他外部資源進行模擬,也不需要安裝代碼,之后也沒有什么需要清理的。

明確地說,冪等性和純函數(shù)只是一種期望,不是必需的。也就是說,由于好處很多,我們可能會希望只編寫純函數(shù)或冪等函數(shù),但這不現(xiàn)實。

重要的是,我們要有意識開始寫代碼來隔離副作用和外部依賴性。這會使得我們編寫的每一行代碼都更容易被測試。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容