# 第五章 條件循環
本章的主題是 if 語句,就是條件判斷,會對應程序的不同狀態來執行不同的代碼。但首先我要介紹兩種新的運算符:floor(地板除法,舍棄小數位)和 modulus(求模,取余數)
## 5.1 地板除和求模
floor 除法,中文有一種翻譯是地板除法,挺難聽,不過湊活了,運算符是兩個右斜杠://,與傳統除法不同,地板除法會把運算結果的小數位舍棄,返回整值。例如,加入一部電影的時間長度是 105 分鐘。你可能想要知道這部電影用小時來計算是多長。傳統的除法運算如下,會返回一個浮點小數:
```py
>>> minutes = 105
>>> minutes / 60
1.75
```
不過一般咱們不寫有小數的小時數。地板除法返回的就是整的小時數,舍棄掉小數位:
```py
>>> minutes = 105
>>> hours = minutes // 60
>>> hours
1
```
想要知道舍棄那部分的長度,可以用分鐘數減去這么一個小時,然后剩下的分鐘數就是了:
```py
>>> remainder = minutes - hours * 60 >>> remainder
45
```
另外一個方法就是使用求模運算符了,百分號%就是了,求模運算就是求余數,會把兩個數相除然后返回余數。
```py
>>> remainder = minutes % 60
>>> remainder
45
```
求模運算符的作用遠不止如此。比如你可以用求模來判斷一個數能否被另一個數整除——比如 x%y 如果等于 0 了,那就是意味著 x 能被 y 整除了。
另外你也可以從一個數上取最右側的一位或多位數字。比如,x%10 就會得出 x 最右邊的數字,也就是 x 的個位數字。同樣的道理,用 x%100 得到的就是右面兩位數字了。
如果你用 Python2 的話,除法是不一樣的。在兩邊都是整形的時候,常規除法運算符/就會進行地板除法,而兩邊只要有一側是浮點數就會進行浮點除法。
## 5.2 布爾表達式
布爾表達式是一種非對即錯的表達式,只有這么兩個值,true(真)或者 false(假)。下面的例子都用了雙等號運算符,這個運算符會判斷兩邊的值是否相等,相等就是 True,不相等就是 False:
```py
>>> 5 == 5
True
>>> 5 == 6
False
```
True 和 False 都是特殊的值,屬于 bool 布爾類型;它們倆不是字符串:
```py
>>> type(True)
<class 'bool'>
>>> type(False)
<class 'bool'>
```
雙等號運算符是關系運算符的一種,其他關系運算符如下:
```py
x != y # x is not equal to y 二者相等
x > y # x is greater than y 前者更大
x > y # x is greater than y 前者更大
x < y # x is less than y 前者更小
x >= y # x is greater than or equal to y 大于等于
x >= y # x is greater than or equal to y 大于等于
x <= y # x is less than or equal to y 小于等于
```
雖然這些運算符你可能很熟悉了,但一定要注意 Python 里面的符號和數學上的符號有一定區別。常見的錯誤就是混淆了等號=和雙等號==。一定要記住單等號=是一個賦值運算符,而雙等號==是關系運算符。另外要注意就是大于等于或者小于等于都是等號放到大于號或者小于號的后面,順序別弄反。
## 5.3 邏輯運算符
邏輯運算符有三種:且,或以及非。這三種運算符的意思和字面意思差不多。比如 x>0 且 x<10,僅當 x 在 0 到 10 之間的時候才為真。
n%2 == 0 或 n%3 == 0,只要條件有一個成立就是真,就是說這個可以被 2 或 3 整除就行了。
最后說這個非運算,是針對布爾表達式的,非(x>y)為真,那么 x>y 就是假的,意味著 x 小于等于 y。
嚴格來說,邏輯運算符的運算對象應該必須是布爾表達式,不過 Python 就不太嚴格。任何非零變量都會被認為是真:
```py
>>> 42 and True
True
```
這種靈活性特別有用,不過有的情況下也容易引起混淆。建議你盡量不要這樣用,除非你很熟練了。
## 5.4 條件執行
有用的程序必然要有條件檢查判斷的功能,根據不同條件要讓程序有相應的行為。條件語句就讓咱們能夠實現這種判斷。最簡單的就是 if 語句了:
```py
if x > 0:
print('x is positive')
```
if 后面的布爾表達式就叫做條件。如果條件為真,隨后縮進的語句就運行。如果條件為假,就不運行。
if 語句與函數定義的結構基本一樣:一個頭部,后面跟著縮進的語句。這樣的語句叫做復合語句。
、復合語句中語句體內的語句數量是不限制的,但至少要有一個。有的時候會遇到一個語句體內不放語句的情況,比如空出來用來后續補充。這種情況下,你就可以用 pass 語句,就是啥也不會做的。
```py
if x < 0:
pass # TODO: need to handle negative values!
```
## 5.5 選擇執行
if 語句的第二種形式就是『選擇執行』,這種情況下會存在兩種備選的語句,根據條件來判斷執行哪一個。語法如下所示:
```py
if x % 2 == 0:
print('x is even')
else:
print('x is odd')
```
I
如果 x 除以 2 的余數為 0,x 就是一個偶數了,程序就會顯示對應的信息。如果條件不成立,那就運行第二條語句。這里條件非真即假,只有兩個選擇。這些選擇也叫『分支』,因為在運行流程上產生了不同的分支。
## 5.6 鏈式條件
有時我們要面對的可能性不只有兩種,需要更多的分支。這時候可以使用連鎖條件來實現:
```py
if x < y:
print('x is less than y')
elif x > y:
print('x is greater than y')
else:
print('x and y are equal')
```
elif 是『else if』的縮寫。這回也還是只會有一個分支的語句會被運行。elif 語句的數量是無限制的。如果有 else 語句的話,這個 else 語句必須放到整個條件鏈的末尾,不過 else 語句并不是必須有的。
```py
if choice == 'a':
draw_a()
elif choice == 'b':
draw_b()
elif choice == 'c':
draw_c()
```
每一個條件都會依次被檢查。如果第一個是假,下一個就會被檢查,依此類推。如果有一個為真了,相應的分支語句就運行了,這些條件判斷的語句就都結束了。如果有一個以上的條件為真,只有先出現的為真的條件所對應的分支語句會運行。
## 5.7 嵌套條件
一個條件判斷也可以嵌套在另一個條件判斷內。上一節的例子可以改寫成如下:
```py
if x == y:
print('x and y are equal')
else:
if x < y:
print('x is less than y')
else:
print('x is greater than y')
```
外部的條件判斷包含兩個分支。第一個分支只有一個簡單的語句。第二個分支包含了另外一重條件判斷,這個內部條件判斷有兩個分支。這兩個分支都是簡單的語句,他們的位置也可以繼續放條件判斷語句的。
雖然語句的縮進會讓代碼結構看著比較清晰明顯,但嵌套的條件語句讀起來還是有點難度。所以建議你如果可以的話,盡量避免使用嵌套的條件判斷。
邏輯運算符有時候對簡化嵌套條件判斷很有用。比如下面這個代碼就能改寫成更簡單的版本:
```py
if 0 < x:
if x < 10:
print('x is a positive single-digit number.')
```
上面的例子中,只有兩個條件都滿足了才會運行 print 語句,所以就用邏輯運算符來實現同樣的效果即可:
```py
if 0 < x and x < 10:
print('x is a positive single-digit number.')
```
這種條件下,Python 提供了更簡潔的表達方法:
```py
if 0 < x < 10:
print('x is a positive single-digit number.')
```
(譯者注:Python 的這種友善度遠遠超過了 C 和 C++,這也是為何我一直建議國內高校用 Python 取代 C++來給本科生和研究生做編程入門課程。)
## 5.8 遞歸運算
一個函數可以去調用另一個函數;函數來調用自己也是允許的。這就是遞歸,是程序最神奇的功能之一,現在可能還不好理解為什么,那么來看看下面這個函數為例:
```py
def countdown(n):
if n <= 0:
print('Blastoff!')
else:
print(n)
countdown(n-1)
```
如果 n 為 0 或者負數,程序會輸出『Blastoff!』。其他情況下,程序會調用自身來運行,以自身參數 n 減去 1 為參數。如果像下面這樣調用這個函數會怎么樣?
```Bash
>>> countdown(3)
```
開始時候函數參數 n 是 3,大于 0,輸出 n 的值 3,然后調用自身,用 n-1 也就是 2 作為參數。。。
接下來的函數參數 n 是 2,大于 0,輸出 n 的值 2,然后調用自身,用 n-1 也就是 1 作為參數。。。
再往下去函數參數 n 是 1,大于 0,輸出 n 的值 1,然后調用自身,用 n-1 也就是 0 作為參數。。。
最后這次函數參數 n 是 0,等于 0 了,輸出『Blastoff!』,然后返回。
n=1 的時候的 countdown 也執行完了,返回。
n=2 的時候的 countdown 也執行完了,返回。
n=3 的時候的 countdown 也執行完了,返回。
(譯者注:這時候一定要注意不是輸出字符串就完畢了,要返回的每一個層次的函數調用者。這里不理解的話說明對函數調用的過程掌握的不透徹,一定要好好想仔細了。)
接下來你就回到主函數 __main__ 里面了。所以總的輸出會如下所示:
```Bash
3
2
1
Blastoff!
```
調用自身的函數就是遞歸的;執行這種函數的過程就叫遞歸運算。
我們再寫一個用 print 把一個字符串 s 顯示 n 次的例子:
```py
def print_n(s, n):
if n <= 0:
return
print(s)
print_n(s, n-1)
s="Python is good"
n=4
print_n(s, n)
```
如果 n 小于等于 0 了,返回語句 return 就會終止函數的運行。運行流程立即返回到函數調用者,函數其余各行的代碼也都不會執行。
函數其余部分的代碼很容易理解:print 一下 s,然后調用自身,用 n-1 做參數來繼續運行,這樣就額外對 s 進行了 n-1 次的顯示。所以輸出的行數是 1+(n-1),最終一共有 n 行輸出。
上面這種簡單的例子,實際上用 for 循環更簡單。不過后面我們就會遇到一些用 for 循環不太好寫的例子了,這些情況往往用遞歸更簡單,所以早點學習下遞歸是有好處的。
## 5.9 遞歸函數的棧圖
在本書的第三章第九節,我們用棧圖來表征函數調用過程中程序的狀態。同樣是這種棧圖,將有助于給大家展示遞歸函數的運行過程。
每次有一個函數被調用的時候,Python 都會創建一個框架來包含這個函數的局部變量和形式參數。對于遞歸函數來說,可能會在棧中同時生成多層次的框架。
圖 5.1 展示了前面樣例中 coundown 函數在 n=3 的時候的棧圖。
________________________________________

Figure 5.1: Stack diagram.
________________________________________
棧圖的開頭依然是主函數 __main__。這里主函數是空的,因為我們沒有在主函數里面創建變量或者傳遞參數進去。
四個 coundown 方框中形式參數 n 的值都是不同的。在棧圖底部是 n=0 的時候,也叫基準條件。這時候不再進行遞歸調用,也就沒有更多框架了。
下面練習一下,畫一個 print_n 函數的棧圖,讓 s 為字符串『Hello』,n 為 2。然后寫一個函數,名字為 do_n,使用一個操作對象和一個數字 n 作為實際參數,給出一個 n 作為次數來調用這個函數。
## 5.10 無窮遞歸
如果一個遞歸一直都不能到達基準條件,那就會持續不斷地進行自我調用,程序也就永遠不會終止了。這就叫無窮遞歸,一般這都不是個好事情哈。下面就是一個無窮遞歸的最簡單的例子:
```py
def recurse():
recurse()
```
在大多數的開發環境下,無窮遞歸的程序并不會真的永遠運行下去。Python 會在函數達到允許遞歸的最大層次后返回一個錯誤信息:
```Bash
File "<stdin>", line 2, in recurse
RuntimeError: Maximum recursion depth exceeded
```
這個追蹤會我們之前看到的長很多。這種錯誤出現的時候,棧中都已經有 1000 層遞歸框架了!
如果你意外寫出來一個無窮遞歸的代碼,好好檢查一下你的函數,一定要確保有一個基準條件來停止遞歸調用。如果存在了基準條件,檢查一下一定要確保能使之成立。
## 5.11 鍵盤輸入
目前為止咱們寫過的程序還都沒有接收過用戶的輸入。這寫程序每次都是做一些同樣的事情。
Python 提供了內置的一個函數,名叫 input,這個函數會停止程序運行,等待用戶來輸入一些內容。用戶按下 ESC 或者 Enter 回車鍵,程序就恢復運行,input 函數就把用戶輸入的內容作為字符串返回。在 Python2 里面,同樣的函數名字不同,叫做 raw_input。
```Bash
>>> text = input()
What are you waiting for?
>>> text
What are you waiting for?
```
在用戶輸入內容之前,最好顯示一些提示,來告訴用戶需要輸入什么內容。input 函數能夠把提示內容作為參數:
```Bash
>>> name = input('What...is your name?\n')
What...is your name?
Arthur, King of the Britons!
>>> name
Arthur, King of the Britons!
```
提示內容末尾的\n 表示要新建一行,這是一個特殊的字符,表示換行。因為有了換行字符,所以用戶輸入就跑到了提示內容下面去了。
如果你想要用戶來輸入一個整形變量,可以把返回的值手動轉換一下:
```Bash
>>> prompt = 'What...is the airspeed velocity of an unladen swallow?\n'
>>> speed = input(prompt)
What...is the airspeed velocity of an unladen swallow?
42
>>> int(speed)
42
```
如果用戶輸入的是其他內容,而不是一串數字,就會得到一個錯誤了:
```py
>>> speed = input(prompt)
What...is the airspeed velocity of an unladen swallow?
What do you mean, an African or a European swallow?
>>> int(speed) ValueError: invalid literal for int() with base 10
```
稍后我們再來看看如何應對這種錯誤。
## 5.12 調試
當語法錯誤或者運行錯誤出現的時候,錯誤信息會包含很多有用的信息,不過信息量太大,太繁雜。最有用的也就下面這兩類:
* 錯誤的類型是什么,以及
* 錯誤的位置在哪里。
```Bash
>>> x = 5
>>> y = 6
File "<stdin>", line 1
y = 6
^
IndentationError: unexpected indent
```
這個例子里面,錯誤的地方是第二行開頭用一個空格來縮進了。但這個錯誤是指向 y 的,這就有點誤導了。一般情況下,錯誤信息都會表示出發現問題的位置,但具體的錯誤可能是在此位置之前的代碼引起的,有的時候甚至是前一行。
同樣情況也發生在運行錯誤的情況下。假設你試著用分貝為單位來計算信噪比。
公式為:

在 Python,你可能像下面這樣寫:
```py
import math
signal_power = 9
noise_power = 10
ratio = signal_power // noise_power
decibels = 10 * math.log10(ratio) print(decibels)
```
運行這個程序,你就會得到如下錯誤信息:
```Bash
Traceback (most recent call last):
File "snr.py", line 5, in ?
decibels = 10 * math.log10(ratio)
ValueError: math domain error
```
這個錯誤信息提示第五行,但那一行實際上并沒有錯。要找到真正的錯誤,就要輸出一下 ratio 的值來看一下,結果發現是 0 了。那問題實際是在第四行,應該用浮點除法,結果多打了一個右斜杠,弄成了地板除法,才導致的錯誤。
所以你得花點時間仔細閱讀錯誤信息,但不要輕易就認為出錯信息說的內容都是完全正確可靠的。
## 5.13 Glossary 術語列表
floor division:
An operator, denoted //, that divides two numbers and rounds down (toward zero) to an integer.
>地板除法:一種運算符,雙右斜杠,把兩個數相除,舍棄小數位,結果為整形。
modulus operator:
An operator, denoted with a percent sign (%), that works on integers and returns the remainder when one number is divided by another.
>求模取余:一種運算符,百分號%,對整形起作用,返回兩個數字相除的余數。
boolean expression:
An expression whose value is either True or False.
>布爾表達式:一種值為真或假的表達式。
relational operator:
One of the operators that compares its operands: ==, !=, >, <, >=, and <=.
>關系運算符:對比運算對象關系的運算符:==相等, !=不等, >大于, <小于, >=大于等于, 以及<=小于等于。
logical operator:
One of the operators that combines boolean expressions: and, or, and not.
>邏輯運算符:把布爾表達式連接起來的運算符:and 且,or 或,以及 not 非。
conditional statement:
A statement that controls the flow of execution depending on some condition.
>條件語句:控制運行流程的語句,根據不同條件有不同語句去運行。
condition:
The boolean expression in a conditional statement that determines which branch runs.
>條件:條件語句所適用的布爾表達式,根據真假來決定運行分支。
compound statement:
A statement that consists of a header and a body. The header ends with a colon (:). The body is indented relative to the header.
>復合語句:包含頭部與語句體的一套語句組合。頭部要有冒號做結尾,語句體相對于頭部要有一次縮進。
branch:
One of the alternative sequences of statements in a conditional statement.
>分支:條件語句當中備選的一系列語句。
chained conditional:
A conditional statement with a series of alternative branches.
>鏈式條件:一系列可選分支構成的條件語句。
nested conditional:
A conditional statement that appears in one of the branches of another conditional statement.
>嵌套條件:條件語句分支中繼續包含次級條件語句的情況。
return statement:
A statement that causes a function to end immediately and return to the caller.
>返回語句:一種特殊的語句,功能是終止當前函數,立即跳出到函數調用者。
recursion:
The process of calling the function that is currently executing.
>遞歸:函數對自身進行調用的過程。
base case:
A conditional branch in a recursive function that does not make a recursive call.
>基準條件:遞歸函數中一個條件分支,要實現終止遞歸調用。
infinite recursion:
A recursion that doesn’t have a base case, or never reaches it. Eventually, an infinite recursion causes a runtime error.
>無窮遞歸:一個沒有基準條件的遞歸,或者永遠無法達到基準條件的遞歸。一般無窮遞歸總會引起運行錯誤。
## 5.14 練習
### 練習 1
time 模塊提供了一個名字同樣叫做 time 的函數,會返回當前格林威治時間的時間戳,就是以某一個時間點作為初始參考值。在 Unix 系統中,時間戳的參考值是 1970 年 1 月 1 號。
(譯者注:時間戳就是系統當前時間相對于 1970.1.1 00:00:00 以秒計算的偏移量,時間戳是惟一的。)
```Bash
>>> import time
>>> time.time() 1437746094.5735958
```
寫一個腳本,讀取當前的時間,把這個時間轉換以天為單位,剩余部分轉換成小時-分鐘-秒的形式,加上參考時間以來的天數。
### 練習 2
費馬大定理內容為,a、b、c、n 均為正整數,在 n 大于 2 的情況,下面的等式關系不成立:
1. 寫一個函數,名叫 check_fermat,這個函數有四個形式參數:a、b、c 以及 n,檢查一下費馬大定理是否成立,看看在 n 大于 2 的情況下下列等式

是否成立。
2. 要求程序輸出『Holy smokes, Fermat was wrong!』或者『No, that doesn’t work.』
3. 寫一個函數來提醒用戶要輸入 a、b、c 和 n 的值,然后把輸入值轉換為整形變量,接著用 check_fermat 這個函數來檢查他們是否違背了費馬大定理。
### 練習 3
給你三根木棍,你能不能把它們拼成三角形呢?比如一個木棍是 12 英寸長,另外兩個是 1 英寸長,這兩根短的就不夠長,無法拼成三角形了。
(譯者注:1 英寸=2.54 厘米)對于任意的三個長度,有一個簡單的方法來檢測它們能否拼成三角形:
只要三個木棍中有任意一個的長度大于其他兩個的和,就拼不成三角形了。必須要任意一個長度都小于兩邊和才能拼成三角形。(如果兩邊長等于第三邊,就只能組成所謂『退化三角形』了。譯者注:實際上這不就成了線段了么?)
1. 寫一個叫做 is_triangle 的函數,用三個整形變量為實際參數,函數根據你輸入的值能否拼成三角形來判斷輸出『Yes』或者『No』。
2. 寫一個函數來提示下用戶,要輸入三遍長度,把它們轉換成整形,用 is_triangle 函數來檢測這些給定長度的邊能否組成三角形。
### 4 練習 4
下面的代碼輸出會是什么?畫一個棧圖來表示一下如下例子中程序輸出結果時候的狀態。
```py
def recurse(n, s):
if n == 0:
print(s)
else:
recurse(n-1, n+s)
recurse(3, 0)
```
1. recurse(-1, 0)這樣的調用函數會有什么效果?
2. 為這個函數寫一個文檔字符串,解釋一下用法(僅此而已)。
接下來的練習用到了第四章我們提到過的 turtle 小烏龜模塊。
### 練習 5
閱讀下面的函數,看看你能否弄清楚函數的作用。運行一下試試(參考第四章里面的例子來酌情修改代碼)。
```py
def draw(t, length, n):
if n == 0:
return
angle = 50
t.fd(length*n)
t.lt(angle)
draw(t, length, n-1)
t.rt(2*angle)
draw(t, length, n-1)
t.lt(angle)
t.bk(length*n)
```
________________________________________

Figure 5.2: A Koch curve.
________________________________________
### 6 練習 6
Koch 科赫曲線是一種分形曲線,外觀如圖 5.2 所示。要畫長度為 x 的這種曲線,你要做的步驟如下:
1. 畫一個長度為三分之一 x 的 Koch 曲線。
2. 左轉 60 度。
3. 畫一個長度為三分之一 x 的 Koch 曲線。
4. 右轉 120 度。
5. 畫一個長度為三分之一 x 的 Koch 曲線。
6. 左轉 60 度。
7. 畫一個長度為三分之一 x 的 Koch 曲線。
特例是當 x 小于 3 的時候:這種情況下,你就可以只畫一個長度為 x 的直線段。
1. 寫一個叫做 koch 的函數,用一個小烏龜 turtle 以及一個長度 length 做形式參數,用這個小烏龜來畫給定長度 length 的 Koch 曲線。
2. 寫一個叫做 snowflake 的函數,畫三個 Koch 曲線來制作一個雪花的輪廓。[參考代碼](http://thinkpython2.com/code/koch.py)
3. The Koch curve can be generalized in several ways. See [here](http://en.wikipedia.org/wiki/Koch_snowflake) for examples and implement your favorite.
生成 Koch 曲線的方法還有很多。點擊 [這里](http://en.wikipedia.org/wiki/Koch_snowflake)來查看更多的例子,探索一下看看你喜歡哪個。