本節書摘來自華章計算機《從問題到程式:用python學程式設計和計算》一書中的第3章,第3.1節,作者:裘宗燕 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
第2章讨論了簡單的計算和程式設計,展示了一些執行個體。通過對有關内容的學習,讀者應該已經做了一些簡單程式,對寫程式和做計算有了些實際體會。雖然程式設計中細節較多,但也是很有趣的工作。為了完成一個程式,首先要分析問題、尋找解決方案,這些需要發揮人的聰明才智和想象力,也可能涉及一些相關領域的知識。要把設計變成可以運作的程式,既需要智力,也需要有條理的工作,一個小錯誤就可能使程式不能正确執行。當然,高度精确性也是現代社會對人的基本要求,寫程式的過程能給我們許多有益的經驗。
學習程式設計要經曆一個過程,但這些并不表示這裡的學習就是簡單的經驗積累,也不意味着隻要多寫程式就一定能把程式寫好。人們通過多年的程式設計實踐,總結出許多帶有規律性的東西,總結出許多程式設計的模式、方法和技術,還進一步研究了許多理論問題。在學習程式設計時,一開始就應該注意前人的經驗。正确的好程式不可能是随便寫出來的,也不應該是修修補補湊出來的。隻有認真學習怎樣寫好小程式,弄清其中的基本道理,才可能進一步寫出更大更複雜的程式。這些都是本書希望特别強調的問題。
在進一步了解python的其他功能之前,初學者需要更好地了解如何把已有基本知識應用于實際,這就是本章的目的。在三種基本的流程模式和相關語言結構中,順序模式最簡單,用複合語句描述也直截了當。使用選擇模式,最重要的問題是正确描述條件,考慮不同情況下的動作,用條件語句實作也不困難。初學程式設計階段的主要難點是重複執行模式。這種模式比較複雜,用循環語句實作時牽涉的問題較多,是本章讨論的第一個重點。
本章将首先讨論編寫循環的一些基本技術,通過一批程式執行個體,展示一些典型的循環程式設計。這裡的方式總是從分析問題開始,逐漸發掘完成程式的線索,最終做出能解決問題的程式,以幫助讀者看到“從問題到程式”的思維過程。對于一些問題執行個體,讨論中給出了能解決問題的多個不同程式,并特别讨論它們之間的差異及優缺點。這樣做是希望讀者了解:程式設計不是教條,也不應死記硬背。即使很典型的問題,也沒有應該記住的标準解答。隻要不是極端簡單的問題,總有許多解決方法,可以寫出許多形式或實質上或多或少不同的程式,而且它們往往各有長短。當然,也不能說這些程式都有同等價值。實際上,正确的程式也可能有優劣之分,讨論中也會對程式的評價提出一些看法。
在第2節裡将介紹一種函數定義技術,稱為遞歸定義,這種技術在處理一些問題時非常有效。最後一節讨論了定義函數的意義和相關的思考和設計問題。本章還将讨論一個非常重要的理論問題:程式的終止性。本章最後還将深入讨論定義函數的一些問題。
前面說過,沒有循環的程式都是平凡的程式。任何複雜一點的計算過程中必然會出現重複性的計算,需要用循環描述。本節集中讨論基于循環的程式設計問題。
3.1.1 循環的需求和問題
在程式裡寫循環,前提是我們發現計算過程中可能需要(或者應該使用)循環。在分析問題時,應該注意識别計算中需要重複執行的類似動作,這種情況說明可能需要引進一個循環,統一描述和處理計算中的一批類似工作。
需要循環的情況
需要寫循環,常見情況如需要一批可以按統一規則計算的結果;需要對一系列類似資料做同樣處理;需要反複從一個結果算出下一結果等。這些都屬于重複性計算,如果重複次數較多,或者次數無法确定,就應考慮用循環。下面是一些典型情況:
1)需要多次執行類似操作而且操作次數較多,适合用循環處理。對這類情況,采用循環通常能縮短程式。用一段公共代碼描述公共操作,更容易檢查、維護和修改。
2)需要做一些重複性的操作,但在程式設計時無法确定操作的次數,結束條件由重複工作中資料的情況決定。這類情況就必須通過循環處理。
3)決定重複控制的因素來自函數的參數或程式的輸入,也必須用循環描述。
舉例來說,假設現在需要制作一個攝氏與華氏的溫度對照表,要求從攝氏0度到200度,每5度一項。這就是典型的第一種情況。而前一章裡通過牛頓疊代法,以不斷改進猜測值的方式求平方根,就是典型的第二種情況。第2章裡也給出了一些循環控制依賴于函數的參數或程式輸入的例子。
寫循環時需要考慮的問題
寫循環的第一步是看到計算中需要做一些重複的操作,而且有規律可循,但從發現計算中的重複性動作,到寫好一個循環結構,通常也不是直截了當的,還需考慮和解決許多具體問題,考慮問題的基礎是計算中需要做什麼。
首先弄清楚需要重複做的處理工作,根據它描述循環的主體。另一方面就是循環的控制,要确定怎樣開始、怎樣繼續或停止循環。這裡有許多具體問題,包括:
為了完成重複性計算,需要為循環引進哪些變量?用什麼變量控制循環?
循環開始前,這些變量應該取什麼值(初值問題)?
在循環體裡(在一次疊代計算中),哪些變量應該更新,其值應該如何變化?
在什麼條件下結束(或者繼續)循環?
循環結束後,如何得到所需的結果?
這些都是本節讨論的重點。還有些具體問題,如需要确定用語言裡的哪種結構實作循環等。如前所述,如果需要重複的次數和方式比較清晰,有可能通過一個循環變量和一個疊代器(如range)控制,就應該用for語句實作,因為它更簡單清晰。如果寫程式時不能确知循環的次數,或循環控制的方式比較複雜,就必須用while語句。
在實際程式設計中,應該采用最簡單、最清晰的循環描述方式,首先考慮用for語句和向上循環(循環變量的值遞增)。在這裡還應該注意python的整數範圍描述方式及其意義。在python語言裡說“從m到n”時總是指序列m, ..., n-1,也就是數學的區間 [m, n),左閉右開,包括m但不包括n。python語言中描述疊代器的range(n)、range(m, n)、range(m, n, d)、描述字元串切片時的參數等,都采用左開右閉的描述方式。後面還會看到許多執行個體。我們在程式設計中也應盡可能采用同樣的規則。
每個循環都有可能用多種不同的方式描述。舉個簡單例子,假設現實中要求對13到26的整數循環(實際要求是包括13和26)。顯然可以用for循環實作,疊代器描述可以用range(13, 27)或者range(26, 12, -1)。按上面說法,如果沒有其他需求,就應該用前者(向上循環)。這類簡單情況一般不考慮while。另一個問題,假設需用while寫一個循環,要求在變量的值大于20時結束,用n <= 20或n < 21作為條件都是正确的。但按照python的習慣,人們贊成後者(左閉右開)。
浮點數與循環控制
由于浮點數計算是近似計算,基于浮點數描述循環控制,需要特别關注計算的不精确問題,否則,寫出的循環很可能沒有反映我們的需求。
首先,range要求一個到三個整數類型的(表達式)參數,不能用浮點數。由于這種規定,在進入用range控制的for語句時,循環的次數就由(當時的)疊代器确定了。在循環裡給循環變量指派不會改變循環的控制情況。例如:
還是會輸出0、2、4、6、8。也就是說,在循環過程中,變量i一次次反複從疊代器得到值,每次得到的新值與當時i的值無關。
現在考慮用浮點數控制while循環的情況。用一個實際例子說明有關問題。
假設現在需要寫一個函數,求sin (x2 + 1)•cos (x) 在 [0, 3] 區間的數值積分,也就是說,求出函數在這個區間的定積分的數值近似。根據高等數學的知識,可以用區間分割法求得這個近似值。現在考慮一種簡單方法,把區間分為100份,基于每個區間左端點的值計算小矩形的面積,累積得到積分值。簡單考慮可能寫出下面程式:
這段程式直接反映了區間分割法的數學定義,循環做到x等于3.0結束。然而,如果把這段程式送給python解釋器,執行将陷入無窮循環。原因很簡單:浮點數是近似計算,從0開始反複加0.03,加100次後結果通常不是3.0,繼續加也不會等于3.0。
這個執行個體說明,不應該用浮點數的等于關系作為循環結束的依據,因為這樣做沒有任何保證。改用小于關系,就可以保證循環一定結束:
這時程式能輸出一個結果,但結果對不對呢 ?
要想計算的結果合理,一個基本要求是循環正好做了100次。如果加入一個計數變量,并在循環體裡将其加一,最後檢查該變量的值,就會發現一個奇怪的情況:這個循環執行了101次,得到的結果顯然不對。這裡的原因也很簡單:由于是近似計算,加100次的結果完全可能大于或小于0.03的100倍。如果仍然小于,就會多循環一次。這個例子說明,用浮點數控制循環的次數,沒有準确而清晰的保證。
解決這類問題,還是應該回到用整數控制循環,下面是一種解決方法:
由于整數計算沒有誤差,采用這種做法,既保證了正确的循環次數,又保證了所用的函數參數盡可能精确(不會積累舍入誤差)。
上面例子反映了用浮點數控制循環的基本問題:計算不精确可能導緻錯過所考慮的精确值,累積誤差可能導緻循環控制的錯誤。總而言之,不應該基于浮點數去描述需要準确控制的循環過程。有些情況應該用浮點表達式控制循環,如前一章求平方根的函數所示,通常用于控制不斷進展(逼近)的疊代,還要設定合理的允許誤差。
3.1.2 常見循環形式
具體循環的情況千差萬别,很難清晰地分類,但我們可以提出一些常見的類型。本節考慮幾種典型的循環情況,供讀者參考。但是,請不要把這裡的分類看作教條,實際程式裡的一個循環有可能屬于這裡的不止一個類型。
簡單重複
需要重複地做一批類似的但又互相完全獨立的工作,是可能用循環描述的最簡單情況。如果這類工作的項數較多,用循環描述比較友善。如果要做的工作項數在程式設計時無法确定(例如,具體情況與輸入有關,或者與函數的參數有關),就必須用循環描述。采用一個描述,除了可能縮短程式外,最大的優勢是把有關處理問題集中到一處,便于設計、實作、調整和修改。如果需要,還可以把有關處理定義為函數,引進新的概念。
簡單重複的特點就是需要對一批資料(或資料組)中的每一項做完全相同的計算,分别得到結果,并且分别使用。這樣,同一循環的不同疊代之間互相無關,反映了原問題中不同工作互相無關的特性。例如,需要反複讀入一批同類資料,對每個(或每組)資料分别處理,将得到的結果輸出,或以同樣方式儲存 。前一章示例中的溫度轉換、作業裡的各種表格生成,都屬于典型的簡單重複計算。
這裡的關鍵是計算或操作采用統一的模式,可以用同一段代碼描述,不同計算之間的差異隻是一個或幾個變量的取值,而這些取值可以按同樣的規律産生,或者按同樣的方式獲得。重複工作的識别和描述都比較簡單,寫好循環的關鍵是總結出共同的計算模式,确定循環中變量取值的變化規律。
下面的循環取自第2章的示例:
它基于使用者輸入的整數,計算階乘并輸出,其中的資料統一通過輸入獲得,主要計算都調用同一個函數完成,輸出的方式和形式相同,是典型的簡單重複模式。另一方面,由于結束條件是根據使用者輸入确定的,程式設計時不知道具體循環次數,應該用while結構。
實際上,互動式方式下python解釋器的最高層就是一個簡單重複計算:反複讀入人的輸入(表達式或語句),執行指令,最後輸出計算的結果。一般互動式程式的最高層也都是這樣,差異隻在于輸入的形式、功能和計算的實作方式不同。
累積
另一類典型的計算工作是累積,其特點就是在重複性的工作中用一個或幾個變量不斷積累資訊。這裡的重複表現為獲得有關資訊的方式類似,将得到的資訊積累到變量裡的方式相同。很顯然,這種工作适合用循環描述,這種循環可稱為累積循環。在這種循環中用于積累資訊的變量可稱為累積變量。
顯然,要想在循環的一次次疊代中不斷将資訊“記入”累積變量中,在循環開始之前,所用的變量就應該有定義,而且針對具體的累積方式設定好初始值。循環的每次疊代中把一些資料的資訊累積到變量裡,更新變量的值。作為累積,所賦新值應包含變量原值的資訊。而(這些)變量的最終值也就是循環的主要結果,在循環結束後使用。
在與數值有關的計算中,累積中常用的操作如 + 或 ,典型操作如x = x + e,其中e為某個表達式,這類指派最适合用擴充指派運算符 +=、= 等描述。這樣寫,既能使描述簡潔,也更好地表現了基于變量原值計算并更新的事實。更一般的累積情況需要特殊的方式,可以考慮定義一個專門的累積函數g,用x = g(x, e)描述更新。
下面看幾個累積程式(片段、函數等)的執行個體。
實際上,在前一章裡已經展示過一些累積循環,例如求階乘函數,其主體就是一個典型的累積循環(現在改用擴充指派運算符描述):
這裡的累積變量(隻有一個)是prod,循環前設定其初值為1,在循環中不斷乘以由循環變量k提供的值(統一更新方式),其最終值就是本函數的結果。
注意,對于累積變量,其初值通常都與循環中的更新方式有關。如果更新采用 +=,相應變量的初值通常總是0,如果更新用 *=,相應變量的初值通常是1。也就是說,按數學中的說法,變量的初值常用更新運算的機關元。
現在考慮數項級數中前n項的計算。例如,數學中有下面公式:
現在考慮定義一個函數計算前num項的值。将循環中的累積變量命名為ln2,由于是求和,其初值取0。相應的循環比較規範,通項可以通過一個取值為1到num(包括num)的循環變量n計算,是以可以用for循環實作。
最後一個問題是通項的符号,可以直接用表達式(-1)**(n-1)計算,進一步寫出程式已經很簡單了。但采用這種做法,每個通項都要進行上述乘幂運算,實際上做了很多乘法。能不能減少這種乘法?可以注意到,這個通項中的下一項的符号總與前一項相反,如果知道目前項的符号,将其乘以-1就得到了下一項的符号。這也是一種資訊的累積。
引入另一個累積變量sign,可以寫出下面的函數定義:
最後的循環是做試驗,程式執行中将輸出:
在實際的計算中,累積工作也可能還有條件,隻有滿足條件的資料才應該累積。此時,就需要在實作這種計算過程的循環中加入條件判斷,隻在資料滿足條件時才更新累積變量。實際上,這是一類很重要的計算模式,稱為生成和篩選。一般情況是,以某種統一的方式不斷獲得(或者直接枚舉出,或者生成、計算出)一些資料,然後用一個篩選條件檢查,隻将滿足條件的資料“記入”累積變量。
下面是一個簡單執行個體。現在希望求出1000以内所有除以7餘數為2的數中不能整除3的整數之和。下面的簡單循環能實作這一計算:
再如,下面程式片段将求出使用者輸入的前10個偶數之和:
這裡的資料來自使用者輸入,程式裡用另一個累積變量(計數變量)n記錄已求和的資料項數,其值達到10就結束。循環結束後,變量sum的值即為所需。從這個程式裡,還可以看到一些新情況:break語句可以嵌套在循環裡任意深的條件結構中,在複雜的條件下退出循環。無論結構多複雜,break總是退出最内層循環。
最後這兩個例子反映了生成和篩選模式的共性情況:循環體中包含一個生成部分,每次産生一項候選資料;而後有一個篩選部分,從得到的資料中選出滿足條件的合格資料。有一些實際情況,其中很難直接生成所需要的資料(資料序列),這時可以考慮找一種較為簡單的生成方式,得到一個更大的資料序列,再從中選出實際需要的資料。
遞推
遞推是一種更一般的循環計算模式,在循環的每次疊代中,基于某個(或者某些)變量的目前值,按某種特定方式算出該變量的下一個值。在實作遞推的循環中,循環體裡總有一個或者一組語句實作如下形式的計算:
也就是說,變量d的下一個值需要通過d本身(和其他資料),利用某種方法計算出來。這種操作實作從d的現值推出其下一個值。這種d也稱為遞推變量。實際上,累積也可以看成一種簡單而且規範的遞推,其計算規則簡單而直接。
遞推變量也需要初值,必須在循環之前給定。有時在一個循環中有可能有多個遞推變量,它們互相扶持、同步前進。某個或某些遞推變量的最終值就是需要的結果。要寫好一個遞推循環,需要做幾件事情:
標明循環中使用的遞推變量;
确定這個(這些)遞推變量的初值;
确定如何從已知資訊(包括遞推變量的目前值)計算出各個遞推變量的下一個值的方法,用程式語句實作相關計算。
第2章中求x平方根的程式包含一個典型的遞推循環。遞推變量guess以1.0作為初值,通過不斷應用疊代規則推出下一個猜測值,最終得到一個滿意的近似值。下面修改過的函數能顯示出遞推的收斂情況:
求sin函數的值
考慮函數sin的計算,數學家提出了下面無窮級數:
現在考慮用這個公式計算sin函數的近似值,要求算到級數項的絕對值小于1e-6。
由于級數項的結構比較複雜,可以考慮定義一個獨立的函數計算它。再根據上述級數定義計算sin的函數,程式寫出如下:
def mysin(x): # 自己定義的計算sin的函數
函數term裡的循環實作除以 (2n+1)!的計算。我們不希望在函數mysin裡兩次計算同一個項,這裡采用先算出項值存入變量,而後檢查和使用的方法。通過幾個常見值的計算,可以看到這個函數定義應該沒錯:
仔細思考上面程式實作的計算過程,其中最重要的部分就是反複算出一個個項的值。可以想到,這裡級數項的計算中出現了許多重複計算:在後一項的計算中,大部分工作都是計算前一項時已經做過的。不難寫出級數項之間的遞推關系:
級數的首項是x,根據遞推公式可以算出随後的各項。
把這個遞推關系融合到mysin的定義裡,修改後的函數定義如下:
通過檢查運作,可以看到這個函數得到的結果與前一個定義相同。在這個函數的執行中避免了大量重複工作,這件事在實踐中非常重要。
有趣的是,做更多的試驗卻可能發現問題:
用更大的值計算,情況更甚。數學上的sin是周期函數,上面計算的正确結果都應該是0,但為什麼會出現誤差越來越大的情況?對20π的計算結果已經毫無意義了,但為什麼會出現這種情況?罪魁禍首應該是計算誤差,但誤差為什麼會積累到這種程度?請讀者仔細分析這兩個表達式的計算過程(提示:請考慮利用python做些試驗)。
要解決上述問題,一種可能方法是用數學包裡的fmod函數對參數規範化,因為sin (x) = sin
(x + 2nπ),可以先把實參歸結到 [0, 2π] 的範圍内。修改後的定義如下:
下面是幾個計算執行個體:
可以看到,上面的問題不再出現了。
這個例子反映出幾個問題:
程式完成後的測試必須比較充分,不僅應該包括最簡單的資料執行個體(如前面的0,pi/2,pi等),還要選擇一些不那麼正常,但也可能出現的資料執行個體。
在浮點數計算中,誤差的積累有可能使結果完全失去價值。對于實際程式裡與浮點數有關的部分,必須針對可能情況做充分的測試。
選擇測試執行個體,應該找那些容易判斷結果正誤,但又能反映測試需要的執行個體。
判斷素數
素數是非常重要的基本數學概念:如果一個大于1的自然數除了1和其自身以外沒有其他因子(沒有真因子),它就被稱為素數(或者質數)。
自然數n的因子,就是能整除n的正整數。而所謂k整除n,也就是說n除以k的餘數為0。在python裡,這一條件可以用表達式n % k == 0描述。
根據上面定義,可以直接得到一種樸素的素數判斷方法:對給定的自然數n,取從2開始的一些整數,逐個檢查它們是否為n的因子。如果發現了n的真因子,那麼n就不是素數。如果一直沒有發現真因子,而且做的試驗已經足夠多,已經能确定n不可能有真因子, 就可以結束檢查并斷定n是素數。
要實作這個判定方法,可以用循環描述檢查的過程。循環中從2開始檢查,最簡單的方法是從小到大檢查每一個整數 。采用這種做法,剩下的問題就是檢查到什麼時候結束?
從2一直檢查到n肯定可以做出正确判斷,但稍微考慮就能看到一個事實:如果n有真因子,那麼它一定有小于等于n的平方根的真因子(請讀者自己證明這個結論)。這樣,檢查循環隻需進行到等于或者超過了n的平方根。
基于上面考慮定義出的判斷函數(謂詞)is_prime如下:
術語謂詞是指傳回邏輯值(真假值)的函數,它們通常用于完成程式中的判斷,經常被用在條件語句或者循環語句的頭部。
基于函數is_prime,很容易寫出一個程式,檢查一定範圍内的偶數,看看哥德巴赫猜想是否成立 。例如下面循環檢查直至200的偶數:
請讀者通過試驗考察前面素數判斷函數的工作效率。不難看到,随着整數值的增大,該函數得到結果的時間越來越長。這件事很自然,例如,對于一個100位的整數,用上面函數判斷,過程中需要檢查1050個數,顯然太耗費時間了。
在有關數論的研究中取得了很多與素數有關的成果,其中一些成果有可能用于素數判斷。基于有關成果,人們提出了一些不同的算法。在網上可以搜尋到許多情況。建議讀者自己找到一些有趣的算法,也可以自己實作一下,作為程式設計練習。
最近對于素數判斷有一個重要進展,2002年印度科學家提出了一個新算法,用發明者的名字首字母縮寫命名,稱為aks算法。該算法在理論上優于已有其他算法。
素數判斷在許多計算領域中有重要的應用,特别是在密碼和安全領域。素數問題及其算法已經成為今天計算機和資訊系統安全的基礎。
3.1.3 輸入循環
與輸入有關的循環是程式裡的一類常見循環。在這種循環中,程式不斷從外部獲得資料,并将其用于内部的計算,得到了足夠多的資料後結束循環,繼續進行循環之後的工作。考慮這類循環的控制和結束,可以看到下面兩種典型情況:
在程式設計時已經明确知道需要輸入的資料的項數情況。循環中隻需一項項讀入資料,完成輸入後結束。這種輸入循環完全由程式内部控制,是最簡單的循環輸入情況,相關程式設計中沒有新問題。當然,這種循環輸入也存在一些變形,見下。
程式設計時隻知道需要從外部輸入一批資料,但并不确知輸入資料的具體項數。前面說過,這種情況隻能通過循環描述。這裡的新問題是如何确定循環應該結束。顯然程式執行中應該反複循環,直到使用者認為應該結束的時刻。也就是說,這是一種由程式外部控制的輸入循環,應稱為由輸入控制的循環。
下面分别讨論與輸入有關的這兩類循環。
簡單輸入循環
簡單輸入循環完全由程式内部确定,這種方式适合在寫程式時就能确定方式的循環。當然,具體方式需要根據實際情況來設計。
例如:假設現在要求獲得一年中各月的降雨量資料,求出全年的降雨量。顯然,這個計算需要正好12項資料。假設資料由使用者通過鍵盤輸入 ,下面程式段能解決這個問題:
對于這類簡單循環,體代碼段的重複執行次數事先已知,原則上說,完全可以将其展開,用該段代碼的一串拷貝實作同樣功能。用循環描述隻是使代碼大大簡化,寫起來也更友善。此外,在這種循環裡,輸入隻作為資訊源,沒有扮演任何特殊角色。
此類簡單循環也有一些可能的擴充方式。一類典型問題是需要從輸入中篩選出“合格”資料,換句話說,假定實際輸入中隻有一部分資料滿足要求,處理過程中需要把“不合格”的資料篩選丢掉。看一個例子。
【例】假定需要輸入一系列數值,求出其中前10項非負資料的平均值。
首先,問題描述已經說明了輸入中可能出現資料為負的情況,而且負數是無用資料,在考慮平均值時應該丢棄。進一步說,雖然程式要求處理10項資料,實際輸入資料的項數卻不可知。還有,在同一程式的不同運作中,需要實際輸入的資料項數可能不同。這些情況說明兩點:首先,這個程式必須用循環實作;其次,for語句不能處理這種情況,隻能用while循環,而且需要自己記錄實際獲得的有效資料項數。
把上述問題都考慮清楚後,可以很自然地寫出下面的程式:
這裡有一個問題值得提出:最後一個語句裡寫的是sum0 / num,寫成sum0 / 10好不好?顯然,對于所給的問題,這兩種寫法都能得到正确結果。但如果把這個num改為10,程式裡就出現了兩個10,而且它們必須保持一緻。如果問題需求有變化,實際情況要求處理12項(而不是10項)資料,我們就必須把這兩個10統一改為12。少改一個程式就錯了(邏輯錯誤),而且不會有任何錯誤資訊。這是很危險的。采用上面程式裡的做法,需求變動時,隻需要做一處修改,循環結束時總能得到正确的平均值。
細心的讀者可能注意到另一個情況:程式裡的兩個輸出字元串中也有10的資訊。這件事也可以解決,請讀者自己設法修改上面程式。
輸入控制的循環
現在考慮另一種常見情況:需要以某種方式統一處理使用者輸入的一批資料,但并不知道使用者将提供多少項資料。顯然,這種情況隻能通過循環,在反複執行中處理輸入。但是,如何控制循環的執行和結束,還是一個需要解決的問題。
這裡的情況意味着輸入提供方(使用者)可以輸入任意多項資料,可以自由決定在任何時刻結束輸入。在這種情況下,顯然,處理輸入的程式不能自行決定結束循環,隻能被動地接受使用者決定。而另一方面,使用者希望結束的意圖也隻能通過輸入傳給程式。這就意味着我們需要為使用者提供一種表達實際資料結束的方式,而且必須采用某種特殊的輸入形式。這樣安排之後,程式在運作中不斷讀入并檢查得到的資料,區分兩種情況:确定是正常輸入時按正常方式處理,一旦遇到表示結束的特殊輸入就結束循環。
這種循環的模式應該是:
如果一個程式在運作中不斷與使用者交換資訊,它就是我們一再說到的互動式程式。使用者啟動這種程式之後,程式将反複向使用者要求資訊。如果得到符合需要的資料,它就完成一定的工作,而後重複這種循環,直至遇到某種預先确定應該結束的情況。顯然,互動式程式中通常都會有一個或多個輸入循環,可以是内部控制的輸入循環,也可以是輸入控制的循環。任何輸入控制的循環都需要确定一個表示結束的特殊輸入。
這種表示正常輸入結束的特殊輸入(特殊指令),實際上是程式與使用者之間的一種約定。使用者需要知道這種約定,藉此表達他們的輸入結束要求。程式也知道(并處理)這種約定,遇到結束信号時就結束循環。作為程式設計者,需要設計這種約定,在程式裡加入對它的檢查,并且告知程式的使用者。這種約定是程式使用方式的重要組成部分。作為一個具體例子,如果在python idle的互動狀态下調用函數quit()或者exit(),python解釋器的互動視窗就會結束執行。這就是idle(的設計者)與我們的約定。
為實作一個具體的這種循環,我們必須設定一種具體約定。顯然,任何正常(正确)的輸入資料都不能作為結束循環的信号。例如,對于一個要求輸入一串整數的程式,我們不能用0作為結束循環的特殊輸入;但如果程式要求的是一串正整數,用0或者負數作為結束信号就都是合理的設計。如果程式的功能是對一系列輸入數值做一些統計工作,例如求它們的平均值和其他統計計算,那麼任何合乎python對數值轉換要求的輸入都是合法資料,是以不能以數值判斷作為結束判斷。python的input函數傳回輸入串,要求程式做
轉換。是以這時就可以考慮用其他形式的字元串作為結束标志,例如:
這樣寫程式,也就是約定用字元串end作為結束标志。為友善使用者,提示符串可以考慮用 "please give next number, 'end' to finish."。相應程式可以寫出如下:
下面是另一個簡單的執行個體:
這個循環要求輸入是字元串,用特殊字元串no more表示不再有更多人名輸入了。最後的輸出語句裡用到字元串拼接,是為了做出符合需要的輸出形式,避免多個輸出項之間不必要的空格。注意,這裡假設了使用者的輸入是人名,而人名就是字元串。由于程式設計時標明的結束約定,如果有個人名叫no more,程式就無法對其表達問候了。
為避免表示結束的字元串不能正常處理的問題,可以考慮另外安排一個輸入,專門處理有關結束的字元串。例如,把前面程式改為:
在這個循環裡,實際資料和控制循環的輸入是分開考慮的。
在實際應用中,輸入未必是字元串,可以是任何資料,甚至可以是若幹資料項的組合。另一方面,處理資料的操作也可以任意複雜。上面兩個例子展示了最常用的兩種控制方式:或者是基于資料輸入自身,或是引入專用于控制是否繼續的輸入。
正如前面的腳注所言,實際程式可能有不同的輸入源。例如程式的輸入可能來自檔案,或者來自網絡,這些輸入也經常需要通過循環處理。雖然輸入源可能不同,但都會遇到上面讨論的問題,都需要有表示輸入結束的約定和相應處理。後面第6章将讨論從檔案輸入的問題,屆時會看到一些情況。