天天看點

《從問題到程式:用Python學程式設計和計算》——2.9 計算的抽象和函數

本節書摘來自華章計算機《從問題到程式:用python學程式設計和計算》一書中的第2章,第2.9節,作者 裘宗燕,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

前面兩節介紹了python語言的所有控制結構。下面先對它們做一些概括和總結,而後介紹控制結構之上的另一類程式設計機制:函數定義。

前面介紹了python語言的三種控制結構,再加上順序執行,總共形成了三種基本的計算流程模式,分别是順序、選擇和重複。python的一些語言結構分别對應于這三種模式。圖2.2畫出了相應計算流程的圖示,這種圖也稱為流程圖。

《從問題到程式:用Python學程式設計和計算》——2.9 計算的抽象和函數

順序計算模式就是做完一個操作之後做下一個操作,如圖2.2a所示。圖中矩形塊表示操作,矩形塊之間的箭頭表示執行的流向。在python裡,位于同一層次的一系列語句描述這種計算模式。注意,由于一系列操作也可以抽象地看作一個組合操作(python裡有相應寫法,見本章最後的“語言細節”一節),如果把圖中從操作2到操作n看作一個操作(如圖中虛線框所示),這裡的一系列操作也可以看作兩個操作的組合。

選擇計算模式根據特定條件,從兩個不同操作中選擇一個執行,如圖2.2b。其中的菱形框表示條件檢查,其兩個出口分别标明真和假,表示條件判斷後的兩個可能流向。python的if語句實作這種計算流程。應該注意,這樣一個複合操作本身也可以抽象地看作一個操作(如虛線框所示),可以放在任何計算流程中作為組成部分。python裡的if結構也是語句,可以出現在程式裡任何要求語句的位置,正反映了上面的認識。還應注意,圖示中的操作并不限于一個語句,允許是任意複雜的組合操作(複雜的語句結構)。

重複計算模式要求根據某種控制方式重複執行操作,如圖2.2c所示,具體控制方式視不同情況而定。python的for和while語句實作重複計算,它們分别采用疊代器或者邏輯條件控制繼續循環體的重複執行或者結束。由于python有break語句,是以實際可以形成的執行流程比這個圖示更複雜一些,但大體情況類似。同樣,一個循環結構也可以作為單元出現在任何其他模式中。python的for和while也是語句,可以作為其他結構的組成部分,形成複雜的嵌套結構。

上述讨論中反複說到的抽象和分解的觀點,在1.3.3節裡讨論過這個問題。在設計一個程式時,經常需要根據問題的情況,将其劃分為順序的一系列較小計算片段(順序計算模式),每個片段看作一個抽象的操作。對每個較小片段,又可能需要按某種模式進一步分解,采用python的if、for或者while結構進一步分解。這樣分解下去,直至計算中的條件和操作都能用python語言的基本功能描述。

上面這種想法非常重要,是開發複雜程式的基本技術。但這樣做還是有缺點:随着一步步的分解,早前的抽象部分被一層層展開,程式也會變得越來越長,越來越複雜。各層次的結構攪在一起,程式的可讀性和易修改性也會随之逐漸惡化。對于開發者而言,直覺的感覺就是這個程式越來越複雜、越來越把握不住、越來越難搞了。

今天的複雜程式可能包含多至百萬、千萬或者上億行代碼。把這麼多代碼寫成一段連續的程式,任何人都無法把握其行為。要克服由程式的複雜性帶來的問題,就需要維持程式中的抽象。就像人們在數學和其他科學研究中定義概念一樣,程式設計也不能一直在語言的基本層面上進行,必須不斷引入新的進階概念,包裝起計算中的複雜性。python中處理這一問題的最基本機制就是下一小節介紹的函數,其他機制将在後面介紹。

函數是python語言的一種重要程式設計機制,用于将一段計算包裝起來,然後就可以以非常友善的方式使用,可以使用一次,也可以使用任意多次。

程式設計語言裡的變量是最基本抽象機制,用于給對象命名。算出一個所需要的值,可能要做很多工作,如果不給它命名,用過就丢了,再次使用就必須重新計算,至少要花費一些代價,程式也會變得更長。把計算出的值(對象)賦給變量,就為其建立了一個抽象。後面再需要這個值就不必重新計算,隻需要寫出相應的變量名,就可以友善地使用了。這是建立值抽象的第一層意義:一次計算,可以任意多次使用。

實際上,為值建立抽象(賦給變量)還有另一層重要意義。舉例說,程式裡通過一段計算得到了值25.4,實際上是一個三角形的面積。把這個值賦給變量area之後,在寫下面的程式時,寫程式的人就可以基于“面積”這個概念思考問題,而不是基于那段很複雜的計算,或者基于形式上沒有反映任何意義的25.4。這個例子也說明變量名的重要作用:合适的名字能成為很好的概念提示,減輕人們在程式設計中的思維負擔。如果到處使用毫無意義的x1、a2或者0、1等,很快就會把自己搞糊塗。古人也知道“名不正言不順”。給每個變量取一個好名字,是寫出好程式的重要一環,絕不可輕視。

然而,需要建立抽象,希望給予适當命名,希望能友善地重複使用的不僅是計算出的值。如前所述,我們還可能希望為(實作一段計算的)一段代碼建立抽象,通過命名友善地重複使用。應該注意,計算的抽象與值的抽象性質不同。舉個例子,根據前面的程式設計經驗,如果能把計算邊長分别為5、7和11的三角形面積的代碼封裝起來,重複使用也就是重新計算這個三角形,意義比較有限。如果能建立一個計算抽象,它能對任給的三邊長計算三角形面積,其作用就很大了。這樣一個計算抽象能解決一個計算問題的所有執行個體。

python函數定義的意義就是能建立起一般計算過程的抽象:寫一次代碼,任意多次重複使用,包括對不同的資料做同樣計算。基于一段代碼定義一個具有計算功能的實體并給予命名,就是定義函數抽象。定義好的函數可以通過名字以簡單的方式使用。

前面我們已經看到了函數的意義,使用過許多内置函數和math程式包的函數。一個函數可以完成某種特定計算,其具體的計算過程可能複雜,但使用非常友善。例如,math庫裡的函數能完成各種數學函數計算。我們不知道其中采用了什麼方法,實作有多複雜。但是,隻需要調用适當的函數,正确提供參數,就能得到所需的結果。此外,在一個程式(或者一個表達式、一個語句)裡,可以任意多次調用同一個數學函數,或調用多個不同的數學函數。由于python系統提供了這些函數,我們就不需要自己費力去實作了。

易見,函數的特點是一次定義,任意使用。開發math函數庫包(以及内置函數等)的人們做了一次,所有使用python的人都可以用,從中獲益。python語言提供的标準庫中有許多有用功能。還有許多人或組織開發了許多有用的第三方python程式包,提供了許多有用功能。有關情況可查庫文檔,搜尋python首頁或者網際網路。

但是,無論python系統及其标準庫提供了多少函數,都不可能滿足所有人的所有需要,其他人已經開發出來的功能也未必合用。為了程式設計的需要,python允許我們自己定義函數,這樣定義的函數稱為自定義函數。實際上,網上可以找到的很多公開資源,也就是其他人用python語言開發的子產品,其中一部分就是他們定義的各種函數。

自定義函數真的很有價值嗎?作為例子,還是考慮前面基于三邊求三角形面積的問題。前面的程式執行一次,能完成一個特定三角形的計算,輸出的結果隻能供人看,無法簡單地繼續使用。而如果有一個函數triangle(a, b, c)計算三角形面積,就可以用它寫出很多有實際意義的有用代碼,例如:

x = triangle(6, 8, 11 # 記錄三角形面積,後面使用

y = triangle(3, 5, 6) * 3 # 一個三角柱體的體積

z = triangle(13, 15, 16) - triangle(6, 8, 11) # 一個空心三角形的面積

... ... # 等等

這樣的例子無窮無盡,而這隻是一個自定義函數。

函數定義也是python的一種複合語句,其執行效果就是定義好一個函數。一個函數定義(語句)包裝起一段代碼,建立起一個函數抽象,并為其命名。

函數定義的文法是:

《從問題到程式:用Python學程式設計和計算》——2.9 計算的抽象和函數

其中函數名是作為被定義函數的名字的辨別符,參數清單是列出的0個或多個名字,多個名字之間用逗号分隔(這是最簡單的情況,更複雜的情況在第5章介紹)。這裡列出的名字稱為所定義函數的形式參數(簡稱形參),整個圓括号及其包起的部分稱為函數的形式參數表。函數定義的最後是一個語句組,稱為函數體。函數體之前的部分稱為函數頭部。

python語言把函數定義也看作一種複合語句,其語義(其執行)就是根據形式參數表和函數體建立一個函數對象,并将其限制到給定的函數名。此後就可以通過函數名使用所定義的函數了。自定義函數同樣通過函數調用的方式使用,其形式前面已介紹:

《從問題到程式:用Python學程式設計和計算》——2.9 計算的抽象和函數

這裡的實際參數是表達式,需要與函數定義的形參比對,最基本的情況就是一個實參對應一個形參(更複雜的情況留待後面章節讨論)。

函數體是個語句組,函數被調用時,就會執行這個語句組。體中的語句執行完畢,一次函數調用就結束了。但這裡還有一個問題:許多函數需要有傳回值,程式設計語言必須提供描述函數傳回值的機制。python語言為此提供了一種語句:函數體裡的return語句有特殊作用,用于描述函數的傳回值。在進一步介紹之前先看一個例子:

【例】三角形面積的函數及其使用

根據上面讨論可以寫出下面代碼:

這裡的def語句定義了一個名字為triangle的函數,函數體照搬前面的代碼,其中從形參出發計算出三角形的面積,最後增加了一個return語句。

随後的三個語句裡都調用剛剛定義的函數,它們分别計算出一些值。調用自定義函數的過程就是把實參的值送給形參,然後執行函數體包裝的語句。執行中遇到return語句時函數結束,求出return所帶表達式的值作為函數的結果傳回。

現在詳細解釋return語句。這種語句隻能出現在函數裡,有兩種形式:

《從問題到程式:用Python學程式設計和計算》——2.9 計算的抽象和函數

無論何時執行到一個return語句,目前函數總是立即結束。如果語句包含表達式部分,在函數結束前先算出這個表達式的值,函數結束後以這個值作為函數的傳回值。如果語句不包含表達式部分,函數結束傳回特殊值none,看作不傳回值。

none是python語言的一個特殊對象,用于表示不存在有意義的值的情況。python解釋器在很多時候也用到這個值。例如指派語句不像普通表達式,在互動方式下執行不會顯示出任何結果。python就讓它的值取none,這個值不顯示。再如:

執行一個函數調用時的詳細情況如下:

1)從左到右逐個求值實際參數(表達式),得到的值賦給函數定義中對應的形參;

2)執行函數體裡的語句;

3)執行中遇return語句時函數的執行立即結束。如果這個return有表達式部分,就求值其表達式作為傳回值,沒有表達式時傳回值為none;

4)所有語句都執行完時函數也結束,傳回值為none。

本小節給出幾個函數定義執行個體。

在前面函數定義裡,用一個指派語句把求出的三角形面積賦給變量area,緊随其後的return語句傳回area的值。可以看到,這個變量并沒有其他作用,隻是臨時記錄一下函數的傳回值。對于這種情況,完全可以不引進新變量,直接把計算傳回值的表達式寫在return語句裡,這裡允許寫任意複雜的表達式:

這個定義比前一個略顯緊湊,計算效率略高。這種做法值得效仿。

前面說過,并不是任意數都是某個三角形的三條邊。在定義計算三角形面積的函數時,也應該考慮這個問題。現在遇到了一個麻煩:計算三角形面積的函數需要傳回一個值,如果參數不符合函數要求,傳回值怎麼辦?數學函數庫的方法是報錯,自定義函數也可以報錯,有關情況後面讨論。現在考慮一個簡單的方法,讓函數傳回一個特殊值。

浮點數計算得不到結果,說明計算中遇到了問題,無法得到可以用浮點數表示的值。在python裡,這種值可以用float("nan")表示,其中的nan是not a number的縮寫(的小寫)形式,表示得到的不是一個float能表示的數值。

利用上述機制定義的函數如下:

注意,由于描述if條件的表達式很長,這裡同樣使用續行。

調用這種函數時有一個麻煩:它可能傳回正常結果,也可能傳回非正常的特殊值。如果能保證送給函數的參數都合适,例如在調用函數之前檢查實參,就不需要擔心第二種情況。計算中出錯是經常需要考慮的情況,python有專門機制處理這方面問題。

有時我們定義函數不是為了計算出一個值,而是希望實作一些操作效果。例如,内置函數print并沒完成什麼有意義的計算,但也很有用。有時我們也需要定義特殊的輸出函數,例如完成某些計算然後輸出結果的函數。

現在考慮定義一個計算三角形面積并且輸出這個面積的函數。根據已有知識,這個函數可以如下定義:

這裡還包含了幾個使用函數的語句,執行這個腳本時就能看到輸出。

自定義輸出函數是一類常見的不傳回值的函數,後面還會看到其他情況。

把計算中的關鍵部分提取出來定義為函數,可以使程式的結構更清晰,意義也更容易了解。例如,下面是另一個階乘電腦腳本,這裡定義了一個函數完成階乘計算,随後的代碼實作循環控制,其中調用前面定義的函數:

與前面實作同樣功能的程式相比,這個程式多了幾行。把階乘計算的部分獨立出來,使之脫離複雜的全局控制結構,這個新寫法使程式清晰得多。可見,雖然有時定義的函數在程式裡隻使用一次,但做出這個函數定義也很值得。

函數可以包裝任意複雜的計算,在函數體的實作中可以使用任何語句,有關語句可以任意地嵌套。此外,return語句不一定出現在函數的最後,可以寫在函數裡的任何地方,包括出現在循環或者其他結構中。