摘要:返回值函數(shù)可以且應該被視為一個獨立的小程序。以下的函數(shù)是冪等的無論何時調用,其返回值都是。
與多數(shù)現(xiàn)代編程語言一樣,在 Python 中,函數(shù)是抽象和封裝的基本方法之一。你在開發(fā)階段或許已經(jīng)寫過數(shù)百個函數(shù),但并非每個函數(shù)都生而平等。寫出「糟糕的」函數(shù)會直接影響代碼的可讀性和可維護性。那么,什么樣的函數(shù)是「糟糕的」函數(shù)呢?更重要的是,要怎么寫出「好的」函數(shù)呢?
簡單回顧
數(shù)學中充滿了函數(shù),盡管我們可能記不住它們。首先來回憶一下大家最喜歡的話題——微積分。你可能記得這個方程式: f(x) = 2x + 3. 這是一個叫做「f」的函數(shù),含有一個未知數(shù) x,「返回」2*x+3。這個函數(shù)可能和我們在 Python 中看到的不一樣,但它的基本思想和計算機語言中的函數(shù)是一樣的。
函數(shù)在數(shù)學中歷史悠久,但在計算機科學中更加神通廣大。盡管如此,函數(shù)還是存在一些缺陷。接下來我們將討論一下什么是「好的」函數(shù),以及在出現(xiàn)什么樣的征兆時我們需要重構函數(shù)。
決定函數(shù)好壞的關鍵
好的 Python 函數(shù)與蹩腳 Python 函數(shù)的區(qū)別是什么?「好」函數(shù)的定義之多讓人驚訝。從我們的目的出發(fā),我會把好的 Python 函數(shù)定義為符合以下清單中大部分規(guī)則的函數(shù)(有些比較難實現(xiàn)):
命名合理
具有單一功能
包含文檔注釋
返回一個值
代碼不超過 50 行
冪等,盡可能是純函數(shù)
對很多人來說,這個列表可能有些過于嚴格。但我保證,如果你的函數(shù)符合這些規(guī)則,你的代碼看起來會非常漂亮。下面我將分步講解各個規(guī)則,然后總結這些規(guī)則如何構成一個「好」函數(shù)。
命名
關于這個問題,我最喜歡的一句話(出自 Phil Karlton,總被誤以為是 Donald Knuth 說的)是:
在計算機科學中只有兩個難題:緩存失效和命名問題。
聽起來有點匪夷所思,但整個不錯的命名真的很難。下面就有一個糟糕的函數(shù)命名:
def?get_knn(from_df):
我基本上在任何地方都見過糟糕的命名,但這個例子來自數(shù)據(jù)科學(或者說,機器學習),從業(yè)者總是在 Jupyter notebook 上寫代碼,然后嘗試將那些不同的單元變成一個可理解的程序。
該函數(shù)命名的第一個問題是使用首字母縮寫/縮略詞。比起縮略詞和并未普及的首字母縮寫,完整的英語單詞會更好。使用縮寫的唯一原因是為了節(jié)省打字時間,但現(xiàn)代的編輯器都有自動補全功能,所以你只需鍵入一次全名。之所以說縮寫是一個問題,是因為它們通常只能用于特定領域。在上面的代碼中,knn 是指「K-Nearest Neighbors」,df 指的是「DataFrame」——無處不在的 Pandas 數(shù)據(jù)結構。如果另外一個不太熟悉這些縮寫的編程人員正在閱讀代碼,那 TA 就會一頭霧水。
關于這個函數(shù)名稱,還有另外兩個小問題:單詞「get」無關緊要。對于大多數(shù)命名比較好的函數(shù),很明顯函數(shù)會返回一些東西,其名字會反映這一點。from_df 也是不必要的。如果參數(shù)的名稱描述不夠清楚的話,函數(shù)的文檔注釋或者類型注釋將描述參數(shù)類型。
那我們如何重新命名這個函數(shù)呢?例如:
def?k_nearest_neighbors(dataframe):
現(xiàn)在,即使是外行也知道這個函數(shù)在計算什么了,參數(shù)的名稱(dataframe)也清楚地告訴我們應該傳遞什么類型的參數(shù)。
單一功能原則
「單一功能原則」來自 Bob Martin「大叔」的一本書,不僅適用于類和模塊,也同樣適用于函數(shù)(Martin 最初的目標)。該原則強調,函數(shù)應該具有「單一功能」。也就是說,一個函數(shù)應該只做一件事。這么做的一大原因是:如果每個函數(shù)只做一件事,那么只有在函數(shù)做那件事的方式必須改變時,該函數(shù)才需要改變。當一個函數(shù)可以被刪除時,事情就好辦了:如果其他地方發(fā)生改動,不再需要該函數(shù)的單一功能,那么只需將其刪除。
舉個例子來解釋一下。以下是一個不止做一件「事」的函數(shù):
def calculate_and print_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)
這一函數(shù)做兩件事:計算一組關于數(shù)字列表的統(tǒng)計數(shù)據(jù),并將它們打印到 STDOUT。該函數(shù)違反了只有一個原因能讓函數(shù)改變的原則。顯然有兩個原因可以讓該函數(shù)做出改變:新的或不同的數(shù)據(jù)需要計算或輸出的格式需要改變。最好將該函數(shù)寫成兩個獨立的函數(shù):一個用來執(zhí)行并返回計算結果;另一個用來接收結果并將其打印出來。函數(shù)有多重功能的一個致命漏洞是函數(shù)名稱中含有單詞「and」
這種分離還可以簡化針對函數(shù)行為的測試,而且它們不僅被分離成一個模塊中的兩個函數(shù),還可能在適當情況下存在于不同的模塊中。這使得測試更加清潔、維護更加簡單。
只做兩件事的函數(shù)其實非常罕見。更常見的情況是一個函數(shù)負責許多許多任務。再次強調一下,為可讀性、可測試性起見,我們應該將這些「多面手」函數(shù)分成一個一個的小函數(shù),每個小函數(shù)只負責一項任務。
文檔注釋
很多 Python 開發(fā)者都知道 PEP-8,它定義了 Python 編程的風格指南,但很少有人了解定義了文檔注釋風格的 PEP-257。在這里并不會詳細介紹 PEP-257,讀者可詳細閱讀該指南所約定的文檔注釋風格。
PEP-8:https://www.python.org/dev/pe...
PEP-257:https://www.python.org/dev/pe...
首先文檔注釋是在定義模塊、函數(shù)、類或方法的第一段字符串聲明,這一段字符串應該需要描述清楚函數(shù)的作用、輸入?yún)?shù)和返回參數(shù)等。PEP-257 的主要信息如下:
每一個函數(shù)都需要一個文檔描述;
使用合適的語法和標點,書寫完整的句子;
最開始需要用一句話總結函數(shù)的主要作用;
使用規(guī)定性的語言而不是描述性的語言。
在編寫函數(shù)時,遵循這些規(guī)則很容易。我們只需要養(yǎng)成編寫文檔注釋的習慣,并在實際寫函數(shù)主體之前完成它們。如果你不能清晰地描述這個函數(shù)的作用是什么,那么你需要更多地考慮為什么要寫這個函數(shù)。
返回值
函數(shù)可以且應該被視為一個獨立的小程序。它們以參數(shù)的形式獲取一些輸入,并返回一些輸出值。當然,參數(shù)是可選的,但是從 Python 內部機制來看,返回值是不可選的。即使你嘗試創(chuàng)建一個不會返回值的函數(shù),我們也不能選擇不在內部采用返回值,因為 Python 的解釋器會強制返回一個 None。不相信的讀者可以用以下代碼測試:
? python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" *for *more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True
運行上面的代碼,你會看到 b 的值確實是 None。所以即使我們編寫一個不包含 return 語句的函數(shù),它仍然會返回某些東西。不過函數(shù)也應該要返回一些東西,因為它也是一個小程序。沒有輸出的程序又會有多少用,我們又如何測試它呢?
我甚至希望發(fā)表以下聲明:每一個函數(shù)都應該返回一個有用的值,即使這個值僅可用來測試。我們寫的代碼應該需要得到測試,而不帶返回值的函數(shù)很難測試它的正確性,上面的函數(shù)可能需要重定向 I/O 才能得到測試。此外,返回值能改變方法的調用,如下代碼展示了這種概念:
with open("foo.txt", "r") as input_file: for line in input_file: if line.strip().lower().endswith("cat"): # ... do something useful with these lines
代碼行 if line.strip().lower().endswith("cat") 能夠正常運行,因為字符串方法 (strip(), lower(), endswith()) 會返回一個字符串以作為調用函數(shù)的結果。
以下是人們在被問及為什么他們寫的函數(shù)沒有返回值時給出的一些常見原因:
「函數(shù)所做的就是類似 I/O 的操作,例如將一個值保存到數(shù)據(jù)庫中,這種函數(shù)不能返回有用的輸出。」
我并不同意這種觀點,因為在操作成功完成時,函數(shù)可以返回 True。
「我需要返回多個值,因為只返回一個值并不能代表什么。」
當然也可以返回包含多個值的一個元組。簡而言之,即使在現(xiàn)有的代碼庫中,從函數(shù)返回一個值肯定是一個好主意,并且不太可能破壞任何東西。
函數(shù)長度
函數(shù)的長度直接影響了可讀性,因而會影響可維護性。因此要保證你的函數(shù)長度足夠短。50 行的函數(shù)對我而言是個合理的長度。
如果函數(shù)遵循單一功能原則,一般而言其長度會非常短。如果函數(shù)是純函數(shù)或冪等函數(shù)(下面會討論),它的長度也會較短。這些想法對于構造簡潔的代碼很有幫助。
那么如果一個函數(shù)太長該怎么辦?代碼重構(refactor)!代碼重構很可能是你寫代碼時一直在做的事情,即使你對這個術語并不熟悉。它的含義是:在不改變程序行為的前提下改變程序的結構。因此從一個長函數(shù)提取幾行代碼并轉換為屬于該函數(shù)的函數(shù)也是一種代碼重構。這也是將長函數(shù)縮短最快和最常用的方法。只要適當給這些新函數(shù)命名,代碼的閱讀將變得更加容易。
冪等性和函數(shù)純度
冪等函數(shù)(idempotent function)在給定相同變量參數(shù)集時會返回相同的值,無論它被調用多少次。函數(shù)的結果不依賴于非局部變量、參數(shù)的易變性或來自任何 I/O 流的數(shù)據(jù)。以下的 add_three(number) 函數(shù)是冪等的:
def add_three(number): """Return *number* + 3.""" return number + 3
無論何時調用 add_three(7),其返回值都是 10。以下展示了非冪等的函數(shù)示例:
def add_three(): """Return 3 + the number entered by the user.""" number = int(input("Enter a number: ")) return number + 3
這函數(shù)不是冪等的,因為函數(shù)的返回值依賴于 I/O,即用戶輸入的數(shù)字。每次調用這個函數(shù)時,它都可能返回不同的值。如果它被調用兩次,則用戶可以第一次輸入 3,第二次輸入 7,使得對 add_three() 的調用分別返回 6 和 10。
為什么冪等很重要?
可測試性和可維護性。冪等函數(shù)易于測試,因為它們在使用相同參數(shù)的情況下會返回同樣的結果。測試就是檢查對函數(shù)的不同調用所返回的值是否符合預期。此外,對冪等函數(shù)的測試很快,這在單元測試(Unit Testing)中非常重要,但經(jīng)常被忽視。重構冪等函數(shù)也很簡單。不管你如何改變函數(shù)以外的代碼,使用同樣的參數(shù)調用函數(shù)所返回的值都是一樣的。
什么是「純」函數(shù)?
在函數(shù)編程中,如果函數(shù)是冪等函數(shù)且沒有明顯的副作用(side effect),則它就是純函數(shù)。記住,冪等函數(shù)表示在給定參數(shù)集的情況下該函數(shù)總是返回相同的結果,不能使用任何外部因素來計算結果。但是,這并不意味著冪等函數(shù)無法影響非局部變量(non-local variable)或 I/O stream 等。例如,如果上文中 add_three(number) 的冪等版本在返回結果之前先輸出了結果,它仍然是冪等的,因為它訪問了 I/O stream,這不會影響函數(shù)的返回值。調用 print() 是副作用:除返回值以外,與程序或系統(tǒng)中其余部分的交互。
我們來擴展一下 add_three(number) 這個例子。我們可以用以下代碼片段來查看 add_three(number) 函數(shù)被調用的次數(shù):
add_three_calls = 0 def add_three(number): """Return *number* + 3.""" global add_three_calls print(f"Returning {number + 3}") add_three_calls += 1 return number + 3 def num_calls(): """Return the number of times *add_three* was called.""" return add_three_calls
現(xiàn)在我們向控制臺輸出結果(一項副作用),并修改了非局部變量(又一項副作用),但是由于這些副作用不影響函數(shù)的返回值,因此該函數(shù)仍然是冪等的。
純函數(shù)沒有副作用。它不僅不使用任何「外來數(shù)據(jù)」來計算值,也不與系統(tǒng)/程序的其它部分進行交互,除了計算和返回值。因此,盡管我們新定義的 add_three(number) 仍是冪等函數(shù),但它不再是純函數(shù)。
純函數(shù)不記錄語句或 print() 調用,不使用數(shù)據(jù)庫或互聯(lián)網(wǎng)連接,不訪問或修改非局部變量。它們不調用任何其它的非純函數(shù)。
總之,純函數(shù)無法(在計算機科學背景中)做到愛因斯坦所說的「幽靈般的遠距效應」(spooky action at a distance)。它們不以任何形式修改程序或系統(tǒng)的其余部分。在命令式編程中(寫 Python 代碼就是命令式編程),它們是最安全的函數(shù)。它們非常好測試和維護,甚至在這方面優(yōu)于純粹的冪等函數(shù)。測試純函數(shù)的速度與執(zhí)行速度幾乎一樣快。而且測試很簡單:沒有數(shù)據(jù)庫連接或其它外部資源,不要求設置代碼,測試結束后也不需要清理什么。
顯然,冪等和純函數(shù)是錦上添花,但并非必需。即,由于上述優(yōu)點,我們喜歡寫純函數(shù)或冪等函數(shù),但并不是所有時候都可以寫出它們。關鍵在于,我們本能地在開始部署代碼的時候就想著剔除副作用和外部依賴。這使得我們所寫的每一行代碼都更容易測試,即使并沒有寫純函數(shù)或冪等函數(shù)。
大家在學python的時候肯定會遇到很多難題,以及對于新技術的追求,這里推薦一下我們的Python學習扣qun:784758214,這里是python學習者聚集地!!同時,自己是一名高級python開發(fā)工程師,從基礎的python腳本到web開發(fā)、爬蟲、django、數(shù)據(jù)挖掘等,零基礎到項目實戰(zhàn)的資料都有整理。送給每一位python的小伙伴!每日分享一些學習的方法和需要注意的小細節(jié)
總結
寫出好的函數(shù)的奧秘不再是秘密。只需按照一些完備的最佳實踐和經(jīng)驗法則。希望這篇文章能夠幫助到大家。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/43878.html
摘要:前言我是,如果你還不認識我,不妨先看看技術的前世今生一平靜的生活已經(jīng)有一段日子了。傳送門技術的前世今生一技術的前世今生三 前言:我是JavaScript,如果你還不認識我,不妨先看看《Web技術的前世今生(一)》 平靜的生活已經(jīng)有一段日子了。 這一天,HTML大哥面露不悅地走過來問我: Js,你是打算和我們分家嗎? 大哥,您這說的哪里話,我什么地方做的不對么?我一臉茫然地回答道。 哼,...
摘要:前言我是,如果你還不認識我,不妨先看看技術的前世今生一平靜的生活已經(jīng)有一段日子了。傳送門技術的前世今生一技術的前世今生三 前言:我是JavaScript,如果你還不認識我,不妨先看看《Web技術的前世今生(一)》 平靜的生活已經(jīng)有一段日子了。 這一天,HTML大哥面露不悅地走過來問我: Js,你是打算和我們分家嗎? 大哥,您這說的哪里話,我什么地方做的不對么?我一臉茫然地回答道。 哼,...
摘要:前言我是,如果你還不認識我,不妨先看看技術的前世今生一平靜的生活已經(jīng)有一段日子了。傳送門技術的前世今生一技術的前世今生三 前言:我是JavaScript,如果你還不認識我,不妨先看看《Web技術的前世今生(一)》 平靜的生活已經(jīng)有一段日子了。 這一天,HTML大哥面露不悅地走過來問我: Js,你是打算和我們分家嗎? 大哥,您這說的哪里話,我什么地方做的不對么?我一臉茫然地回答道。 哼,...
摘要:后來的對話中,面試官也表示,可能之前做的更多的是的工作,對于容器這塊不熟悉關系也不是很大。 showImg(https://segmentfault.com/img/remote/1460000018525265?w=1718&h=808); 這次給大家講講我2年前去愛奇藝面試高級運維開發(fā)崗位的經(jīng)歷,希望對大家?guī)硪恍椭?公眾號「Python專欄」后臺回復:自動化運維平臺,獲取整套...
摘要:代碼規(guī)范的重要性,意義,就不在這里廢話了,今天只講講我的經(jīng)歷。舉個栗子項目中,一個用戶的標識,改用什么字段表示呢這么柚子的問題你也來問。 代碼規(guī)范的重要性,意義,就不在這里廢話了,今天只講講我的經(jīng)歷。 舉個栗子:項目中,一個用戶的標識,改用什么字段表示呢? are you kiding me? 這么柚子的問題你也來問。 嗯,就是這個柚子的問題,那么問題來了 ,你會用什么字段表示呢? 據(jù)...
閱讀 1458·2021-11-24 09:39
閱讀 1775·2021-11-22 15:25
閱讀 3728·2021-11-19 09:40
閱讀 3283·2021-09-22 15:31
閱讀 1288·2021-07-29 13:49
閱讀 1192·2019-08-26 11:59
閱讀 1308·2019-08-26 11:39
閱讀 919·2019-08-26 11:00