本節書摘來自華章計算機《從問題到程式:用python學程式設計和計算》一書中的第2章,第2.8節,作者:裘宗燕 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
在前面幾節,我們首先看到如何通過語句的順序組合構造最簡單的程式,這種程式是直線型程式,就是簡單的一系列語句。這樣的程式中隻有一條執行路徑(一種可能執行方式):python解釋器順序執行程式裡的語句,每個語句執行一次,當語句序列中最後一條語句的執行結束時,整個程式的執行就結束了。
增加了if複合語句,能寫出的程式更多,程式的形式也更豐富,其中出現了選擇和分支。這樣得到的程式可稱為分支程式。在分支程式裡,每條基本語句最多執行一次,如果實際條件導緻的執行沒進入某個分支,該分支裡的語句就不會執行。這說明在處理不同資料時,實際執行的語句序列(執行路徑)有可能不同,存在多條不同執行路徑。但事情也很清楚,一個分支程式裡的所有可能執行路徑是可以列舉出來的。
直線型程式和分支程式的描述比較簡單,能完成的工作也簡單,隻是一組基本操作(包括表達式求值、變量指派、輸出輸入等)的順序組合。注意,分支隻是表達了對不同基本操作或操作序列的選擇。程式裡的每個基本語句至多執行一次,程式的長度(語句的總條數)是所有執行路徑長度的上限。這樣,程式的功能就受到程式長度的限制,能完成的工作隻是程式裡基本操作的不同組合。我們寫程式用了很多時間,解釋器一瞬間就執行完了。要使寫出的程式能完成更複雜的計算,就必須突破這種狀況。
2.8.1 重複計算
最基本的複雜計算就是重複計算,例如:
求1到10的整數之和;
求2到30的偶數之乘積;
求1 – 1/3 + 1/5 – 1/7 + 1/9 …– 1/19,等等。
這裡的基本情況是:
需要做一系列重複性的計算;
計算中需要做的操作有規律,可以說清楚。
需要重複計算的情況很多,可以舉出其中的一些類别。最簡單的一類情況是寫程式時能确定需要重複的次數。對這類情況,通過一個可能較長的表達式,或者通過一系列語句,可能描述這種重複計算。例如求1到10的十個整數之和,可以寫:
或者
但是,這些寫法都太繁瑣,也不能處理一般情況。按前面的說法,對連續的一個整數區間裡的值求和是一個計算問題,求1到10的整數之和是其具體執行個體。上面程式隻是解決了一個具體執行個體,沒有太大用。而實際中可能有不同的情況和需要。例如,要求和的整數區間可能由輸入得到,按上面方式就無法寫出解決問題的程式了。當然,對上面這個簡單問題,存在求和公式,但這種情況并不具有一般性。
計算中需要做重複性工作是計算的本質,各種程式設計語言(包括python)都為描述重複計算提供了專門的結構,稱為循環結構或循環語句。循環語句是python裡的一類複合語句,其性質與順序和分支結構不同。在一個循環語句的執行中,其成分語句可能被多次執行,這就使很短的程式可能導緻非常長的操作執行序列。
python語言裡有兩種循環語句:for語句用于描述比較簡單比較規範的循環;while語句用于描述一般的複雜循環。它們都控制一個語句組的重複執行。
2.8.2 for語句和重複計算
for語句用一個循環控制器(在python裡稱為疊代器)描述其成分語句組的重複執行方式。其基本文法形式是:
for和in都是關鍵字,語句中包含了三個成分,最關鍵的是疊代器。由關鍵字for開始的行稱為循環的頭部,語句組稱為循環體。與if語句的情況類似,這裡語句組中的語句也是下一層的成分,同樣需要倒退,組中各語句必須互相對齊。
疊代器是python語言裡的一類重要機制。一個疊代器描述一個值序列(也就是說,一系列的對象),可以在一些特殊上下文中使用,例如用在for語句的頭部。for語句的語義是:讓變量順序取得疊代器表示的值序列中的各個值,對每個值執行語句組一次。由于循環體裡可以使用頭部引進的變量,在循環體每次執行時,該變量的值可能不同,是以,雖然反複執行的是同一段語句,但每次執行的效果卻可能不同。
下面是使用for語句的一個簡單執行個體,它求出0到99這100個數之和:
這個for語句的循環體裡隻有一個語句,每次執行把變量x的值加在變量s原有的值之上,在循環語句開始執行前給s指派0。
要了解上述程式段,最重要的問題就是range(100)的意義。range是python的一個内置函數,調用這個函數,就能得到一個疊代器(是以适合放在for語句頭部)。函數range有幾種不同調用方式,不同方式得到的疊代器情況如下:
range(n) 得到的疊代器表示序列0, 1, 2, ……, n-1。是以range(100) 表示序列0、1、2、……、99。
range(m, n) 得到的疊代器表示序列m, m+1, m+2, ……, n-1。例如range(10, 16)表示序列10、11、12、13、14、15。
range(m, n, d) 得到的疊代器表示等差序列m, m+d, m+2d, ……,按步進值d遞增(如果d為負就是遞減),直至那個最接近但不包括n的等內插補點。例如,range(10, 16, 2) 表示的序列是10、12、14,而range(15, 4, -3) 表示的序列是15、12、9、6。
對于range(n),在n不大于0時序列為空。如果用這樣的疊代器控制for循環,其循環體将一次也不執行,整個for語句立即結束。對于range(m, n),如果m≥n則序列為空。對于range(m, n, d),d可以是正整數或負整數,表示增量是向上或者向下,也可能出現空序列的情況。很容易看清楚,這裡range函數的參數也描述了一個左閉右開的區間(或等差序列區間),與字元串切片的情況類似。
在上面for循環的循環體裡,隻寫了一個給變量s指派的語句。實際執行時,每次執行這個語句就會給s賦一個新值。在循環中反複修改(更新)一個或幾個變量的值,是程式裡最常用的一種技術。下面會看到很多這樣的例子。易見,簡單修改程式中range函數的參數,同一個程式就能完成對不同整數區間的求和。進而,用變量控制疊代器的範圍,同一個程式就能在不同的執行中完成對不同整數區間的求和。這些說明,有了循環語句,我們對計算機的工作方式和過程進行控制、安排其執行方式的能力大大增強了。
下面是一個一般的等差整數序列的求和程式:
這裡range的參數用b+1,因為range産生的序列總不包括這個參數值。
現在考慮一個實際問題:通過輸入指定對照表的範圍和溫度值間隔,生成攝氏與華氏溫度的對照表。一方面,這個問題與前一個類似,要求對一系列值做同樣計算。但又不同,其中對不同值的計算互相無關,隻是用了同一個計算公式。
從攝氏溫度c到華氏溫度f的計算公式是:
很容易寫出下面程式:
如果這個程式運作時依次輸入0、50、10,它将輸出:
考慮另一個計算問題:求階乘。由數學可知,正整數n的階乘是:
0的階乘定義為1。完成這個計算需要乘起一系列整數,直至某個在寫程式時不能确定的n。這種工作必須用循環處理。在重複計算中需記錄不斷增長的部分乘積,使用一個變量。
基于for循環實作這一計算,還需要确定range的調用方式,做出正确的疊代器(生成正确的整數序列)。考慮了各種情況後寫出的程式如下:
如果認為需要,也可以加入檢查輸入的結構。這裡實際上假設了輸入是非負整數,如果輸入的n值是0或1,range(2, n + 1) 生成的疊代器産生空序列,循環立即結束,結果正好為所需。用range(1, n + 1) 也對,不改變程式的意義。
下面程式重複三次要求輸入和求階乘的計算:
在這段程式裡,一個for循環的循環體裡出現了另一個for循環。由于for循環也是語句,可以出現在任何要求寫語句的程式結構裡,包括循環體。有人把這種情況稱為嵌套循環,或者多重循環。實際上,這種情況并不特殊。
在執行中,上面程式與使用者互動三次,每次通過輸入得到一個整數,完成階乘計算後輸出一個結果。這是一個簡單的階乘電腦,隻能做3次階乘。
最後再強調一下,内置函數range的使用,實際上反映了python有關一個值序列描述的原則:描述的總是左閉右開的序列。後面還會多次遇到類似的情況。
2.8.3 while語句和疊代
另一種循環語句是while語句,它用一個表示邏輯條件的表達式控制循環,在條件成立的情況下反複執行循環體,直至條件不成立時結束。while語句的形式很簡單:
解釋器執行while語句時首先求值其條件表達式,如果值為真就執行循環體語句組一次,然後重複上述動作;表達式的值為假時while語句立刻結束。
顯然,while語句可以實作for語句能實作的所有計算,例如,不難基于while語句寫出另一個求階乘的程式,其中用一個變量記錄不斷增長的乘數:
與前面采用for語句的程式相比,采用while語句時,必須自己管理循環中使用的變量i,自己做增量操作。這樣寫出的程式不如前一個簡單。
如果循環比較規範,循環中的控制比較簡單,事先可以确定循環次數,人們提倡用for語句而不用while語句,因為用前者寫出的程式往往更簡單,也更清晰。但是,也有一些循環無法用for語句寫出,必須用while描述。
舉個例子說明這種情況。現在計劃開發另一個更好的階乘電腦,希望它能任意次地接受輸入并計算階乘值,直至使用者希望結束為止。
這裡的情況與前面三次計算階乘的程式有些類似,需要把一段計算階乘的代碼包在一個循環裡。但是現在循環次數無法事先确定,而且,這個程式每次執行中需要計算階乘的次數有可能不同,根據使用者的需要确定。另外,我們也不要求使用者在程式開始時先決定計算的次數,并把這個次數告知(通過輸入)計算階乘的程式,而是允許使用者在程式工作中随時提出結束的要求。為了處理這種情況,我們需要給使用者提供一種表達結束的方式。由于負數的階乘沒有定義,我們可以規定一旦使用者輸入負數,循環就結束,而是否為負數就是電腦繼續工作(重複執行)的條件,可以用一個while語句描述。
确定了上面的問題解決方案之後,寫出程式已經不困難了:
下面是本程式一次執行的情況:
這是一個典型的互動式的計算程式,它不斷要求輸入,得到一個整數就做計算,而後輸出結果。實際上,這個程式可以看作是一台專用的計算機——階乘計算機,其行為恰如第1章的圖1.2所示。這台計算機的指令就是正整數,任何負整數都是它的停機指令。它的功能就是一條條地執行“指令”(計算階乘)并輸出結果。
上述程式有一個缺點:同樣的輸入語句在程式裡寫了兩次。多次重複寫同樣代碼是不好的現象。以上面程式為例,如果我們想修改提示符,就需要在兩個地方同時改,還需要維護一緻性。這種情況應該盡可能避免。然而,采用直至目前已經介紹的機制,上面程式的問題還沒有解決辦法。後面介紹的python功能可以解決這個問題。
還有一大類計算問題需要使用while循環。對于這類問題,人們通過研究設計出一套反複計算的規則,稱為疊代規則,并證明了反複使用規則就一定能得到解,但何時結束要看實際計算的實際進展情況。下面考慮一個具體問題。
現在考慮求實數平方根的問題。人們提出的計算規則如下:
假設要求實數x的平方根,任取y為某個實數值
如果y×y=x,計算結束,y就是x的平方根
令z=(y+x/y)/2
令y的新值為z,轉回步驟1
通過這樣反複計算,可以得到一個y值的序列。人們已經證明這個序列将趨向于x的平方根。這種方法稱為計算平方根的牛頓疊代法。
根據上述方法可以直接寫出一個程式,顯然其中需要循環。程式如下:
這裡的guess表示對平方根的猜測,在循環的反複疊代執行中,這個猜測值被不斷改善,在其平方不等于x的情況下繼續改善。
上述程式的寫法應該沒錯(很簡單,是牛頓疊代法的直接翻譯),運作後給輸入2.0,我們會發現程式一直不能給出結果,看來可能是進入了死循環。在idle裡可以通過ctrl-c組合鍵中斷正在執行的程式。現在修改程式,在循環中加一個輸出語句:
可以看到,程式反複輸出同樣資訊(在作者機器上是1.414213562373095 1.9999999999 999996),變量guess的平方總也不能恰好等于2.0,循環無法終止。
實際上,這裡遇到的也是近似計算帶來的問題。由于2.0的平方根是無理數,浮點數隻能表示其近似值,而且精度有限,再加上所做的乘法是近似計算。兩者疊加,疊代中變量值的平方總不能等于2.0,導緻循環永遠也無法結束。
浮點數計算是近似的,使用浮點計算,隻能希冀得到近似的結果。由于這個情況,對求平方根問題,我們隻能考慮在guess的平方足夠接近x時結束,不能用等于判斷。例如,可以取兩者誤差的絕對值不超過10-8。基于這個想法,很容易寫出下面程式:
對給定浮點數,這個程式很快就能算出結果。這裡的abs是前面介紹過的python内置函數,它求出參數的絕對值。顯然其參數應該是數值。
為了了解程式疊代的情況,我們可以修改程式,讓它多輸出一些資訊:
這裡增加了一個計數變量,幫助統計循環的次數,用print同時輸出當時的guess值,使人可以看到猜測值逼近最終結果的過程。對浮點數2.0的試驗情況如下:
疊代4次就得到了結果,說明牛頓疊代法計算收斂得非常快。讀者可以修改程式的精度要求,或者換一些其他數值做試驗,觀察程式執行情況的變化。
python的一個優點是很容易做各種計算試驗,本質上是利用計算機可程式設計的優點。但python語言的設計使人在做這些事情時非常友善。讀者可以自己考慮一些有趣的計算問題,做些試驗,觀察分析計算中的情況和問題。
2.8.4 循環控制
for語句和while語句都是通過頭部控制循環的進行,一旦執行進入循環體,就會完整地執行一遍其中的語句,然後再重複。實際中也存在一些情況,其中在一定條件下應該隻執行循環體的一部分,然後就退出循環,或者立刻轉去做循環的下一次疊代。為了滿足這類需要,python提供了兩個特殊的循環控制語句。
循環中斷語句的形式很簡單,隻是一個關鍵字break。如果在循環體的執行中遇到這個語句,目前的循環語句立刻結束,解釋器轉到循環之後的語句。
循環繼續語句的形式也很簡單,就是一個關鍵字continue,如果在循環體執行中遇到這個語句,循環體的本次執行結束,執行回到循環頭部,根據頭部的要求繼續。
顯然,這兩個語句都隻能出現在循環體裡面,而且隻能控制包含着它們的最内層循環語句(請注意,循環可以嵌套)。此外,這兩個語句通常總是出現在某個條件語句裡,在一定條件下才做相應的動作。兩者中break使用得更廣泛一些。
現在考慮前面遺留的一個問題,利用break改造前面的階乘電腦,消除其中重複的語句。由于用break可以從循環的中間退出,下面程式能完成同樣的工作:
這裡的循環條件是true,表示條件永遠成立,這個循環不會由于頭部檢查而結束。但由于在循環裡出現了帶條件的break語句,一旦其條件成立,循環就會結束。是以,不能說這是一個死循環,隻要計算中break的條件成立,循環就會結束。對于這個例子,隻要使用者輸入小于0,執行就會到達break語句,循環就結束了。
後面會看到更多使用break的例子,也可以看到使用continue的例子。