# 第六章 有返回值的函數
我們已經用過的很多 Python 的函數,比如數學函數,都會有返回值。但我們寫過的函數都是無返回值的:他們實現一些效果,比如輸出一些值,或者移動小烏龜,但他們就是不返回值。
## 6.1 返回值
對函數進行調用,就會產生一個返回的值,我們一般把這個值賦給某個變量,或者放進表達式中來用。
```py
e = math.exp(1.0)
height = radius * math.sin(radians)
```
目前為止,我們寫過的函數都沒有返回值。簡單說是沒有返回值,更確切的講,這些函數的返回值是空(None)。
在本章,我們總算要寫一些有返回值的函數了。第一個例子就是一個計算給定半徑的圓的面積的函數:
```py
def area(radius):
a = math.pi * radius**2
return a
```
返回語句我們之前已經遇到過了,但在有返回值的函數里面,返回語句可以包含表達式。這個返回語句的意思是:立即返回下面這個表達式作為返回值。返回語句里面的表達式可以隨便多復雜都行,所以剛剛那個計算面積的函數我們可以精簡改寫成以下形式:
```py
def area(radius):
return math.pi * radius**2
```
另外,有一些臨時變量可以讓后續的調試過程更簡單。所以有時候可以多設置幾條返回語句,每一條都對應一種情況。
```py
def absolute_value(x):
if x < 0:
return -x
else:
return x
```
因為這些返回語句對應的是不同條件,因此實際上最終只會有一個返回動作執行。
返回語句運行的時候,函數就結束了,也不會運行任何其他的語句了。返回語句后面的代碼,執行流程里所有其他的位置都無法再觸碰了,這種代碼叫做『死亡代碼』。在有返回值的函數里面,建議要確認一下每一種存在的可能,來讓函數觸發一個返回語句。下面例子中:
```py
def absolute_value(x):
if x < 0:
return -x
if x > 0:
return x
```
這個函數就是錯誤的,因為一旦 x 等于 0 了,兩個條件都沒滿足,沒有觸發返回語句,函數就結束了。執行流程走完這個函數之后,返回的就是空(None),而本應該返回 0 的絕對值的。
```py
>>> absolute_value(0)
>>> absolute_value(0)
None
```
順便說一下,Python 內置函數就有一個叫 abs 的,就是用來求絕對值的。
然后練習一下把,寫一個比較大小的函數,用兩個之 x 和 y 作為參數,如果 x 大于 y 返回 1,相等返回 0,x 小于 y 返回-1.
## 6.2 增量式開發
寫一些復雜函數的時候,你會發現要花很多時間調試。
要應對越來越復雜的程序,你不妨來試試增量式開發的辦法。增量式開發的目的是避免長時間的調試過程,一點點對已有的小規模代碼進行增補和測試。
$$distance = \sqrt{(x_2 ? x_1)^2 + (y_2 ? y_1)^2}$$
首先大家來想一下用 Python 來計算兩點距離的函數應該是什么樣。換句話說,輸入的參數是什么,輸出的返回值是什么?
這個案例里面,輸入的應該是兩個點的坐標,平面上就是四個數字了。返回的值是兩點間的距離,就是一個浮點數了。
```py
def distance(x1, y1, x2, y2):
return 0.0
```
當然了,上面這個版本的代碼肯定算不出距離了;不管輸入什么都會返回 0 了。但這個函數語法上正確,而且可以運行,這樣在程序過于復雜的情況之前就能及時測試了。
要測試一下這個新函數,就用簡單的參數來調用一下吧:
```py
>>> distance(1, 2, 4, 6)
0.0
```
我選擇這些數值,水平的距離就是 3,豎直距離就是 4;這樣的話,平面距離就應該是 5 了,是一個 3-4-5 三角形的斜邊長了。我們已經知道正確結果應該是什么了,這樣對測試來說很有幫助。
現在我們已經確認過了,這個函數在語法上是正確的,接下來我們就可以在函數體內添加代碼了。下一步先添加一下求 x2-x1 和 y2-y1 的值的內容。接下來的版本里面,就把它們存在一些臨時變量里面,然后輸出一下。
```py
def distance(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
print('dx is', dx)
print('dy is', dy)
return 0.0
```
這個函數如果工作的話,應該顯示出'dx is 3'和'dy is 4'。如果成功顯示了,我們就知道函數已經得到了正確的實際參數,并且正確進行了初步的運算。如果沒有顯示,只要檢查一下這么幾行代碼就可以了。接下來,就要計算 dx 和 dy 的平方和了。
```py
def distance(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dsquared = dx**2 + dy**2
print('dsquared is: ', dsquared)
return 0.0
```
在這一步,咱們依然虧運行一下程序,來檢查輸出,輸出的應該是 25。輸出正確的話,最后一步就是用 math.sqrt 這個函數來計算并返回結果:
```py
def distance(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
dsquared = dx**2 + dy**2
result = math.sqrt(dsquared)
return result
```
如果程序工作沒問題,就搞定了。否則你可能就需要用 print 輸出一下最終計算結果,然后再返回這個值。
此函數的最終版本在運行的時候是不需要顯示任何內容的;這個函數只需要返回一個值。我們寫得這些 print 打印語句都是用來調試的,但一旦程序能正常工作了,就應該把 print 語句去掉。這些 print 代碼也叫『腳手架代碼』因為是用來構建程序的,但不會被存放在最終版本的程序中。
當你動手的時候,每次建議只添加一兩行代碼。等你經驗更多了,你發現自己可能就能夠駕馭大塊代碼了。不論如何,增量式開發總還是能幫你節省很多調試消耗的時間。
這個過程的核心如下:
1. 一定要用一個能工作的程序來開始,每次逐漸添加一些細小增補。在任何時候遇到錯誤,都應該弄明白錯誤的位置。
2. 用一些變量來存儲中間值,這樣你可以顯示一下這些值,來檢查一下。
3. 程序一旦能工作了,你就應該把一些發揮『腳手架作用』的代碼刪掉,并且把重復的語句改寫成精簡版本,但盡量別讓程序變得難以閱讀。
做個練習吧,用這種增量式開發的思路來寫一個叫做 hypotenuse(斜邊)的函數,接收兩個數值作為給定兩邊長,求以兩邊長為直角邊的直角三角形斜邊的長度。做練習的時候記得要記錄好開發的各個階段。
## 6.3 組合
你現在應該已經能夠在一個函數里面調用另外一個函數了。下面我們寫一個函數作為例子,這個函數需要兩個點,一個是圓心,一個是圓周上面的點,函數要用來計算這個圓的面積。
假設圓心的坐標存成一對變量:xc 和 yc,圓周上一點存成一對變量:xp 和 yp。第一步就是算出來這個圓的半徑,也就是這兩個點之間的距離。我們就用之前寫過的那個 distance 的函數來完成這件事:
```py
radius = distance(xc, yc, xp, yp)
```
下一步就是根據計算出來的半徑來算圓的面積;計算面積的函數我們也寫過了:
```py
result = area(radius)
```
把上述的步驟組合在一個函數里面:
```py
def circle_area(xc, yc, xp, yp):
radius = distance(xc, yc, xp, yp)
result = area(radius)
return result
```
臨時變量 radius 和 result 是用于開發和調試用的,只要程序能正常工作了,就可以把它們都精簡下去:
```py
def circle_area(xc, yc, xp, yp):
return area(distance(xc, yc, xp, yp))
```
## 6.4 布爾函數
函數也可以返回布爾值,這種情況便于隱藏函數內部的復雜測試。例如:
```py
def is_divisible(x, y):
if x % y == 0:
return True
else:
return False
```
一般情況下都給這種布爾函數起個獨特的名字,比如要有判斷意味的提示;is_divisible 這個函數就去判斷 x 能否被 y 整除而對應地返回真或假。
```py
>>> is_divisible(6, 4)
False
>>> is_divisible(6, 3)
True
```
雙等號運算符的返回結果是一個布爾值,所以我們可以用下面的方法來簡化剛剛的函數:
```py
def is_divisible(x, y):
return x % y == 0
```
布爾函數經常用于條件語句:
```py
if is_divisible(x, y):
print('x is divisible by y')
```
可以用于寫如下這種代碼:
```py
if is_divisible(x, y) == True:
print('x is divisible by y'
```
但額外的比較并沒有必要。
做一個練習,寫一個函數 is_between(x, y, z),如果 x ≤ y ≤z 則返回真,其他情況返回假。
## 6.5 更多遞歸
我們目前學過的知識 Python 的一小部分子集,不過這部分子集本身已經是一套完整的編程語言了,這就意味著所有能計算的東西都可以用這部分子集來表達。實際上任何程序都可以改寫成只用你所學到的這部分 Python 特性的代碼。(當然你還需要一些額外的代碼來控制設備,比如鼠標、磁盤等等,但也就這么多額外需要而已。)
阿蘭圖靈最先證明了這個說法,他是最早的計算機科學家之一,有人會認為他更是一個數學家,不過早起的計算機科學家也都是作為數學家來起步的。因此這個觀點也被叫做圖靈理論。關于對此更全面也更準確的討論,我推薦一本 Michael Sipser 的書:Introduction to the Theory of Computation 計算方法導論。
為了讓你能更清晰地認識到自己當前學過的這些內容能用來做什么,我們會看看一些數學的函數,這些函數都是遞歸定義的。遞歸定義與循環定義有些相似,就是函數的定義體內包含了對所定義內容的引用。一一個完全循環的定義并不會有太大用。
vorpal:
An adjective used to describe something that is vorpal.
>刺穿的:用來描述被刺穿的形容詞。
你在詞典里面看到上面這種定義,一定很郁悶。然而如果你查一下階乘函數的定義,你估計會得到如下結果:
0! = 1
n! = n (n?1)!
這個定義表明了 0 的階乘為 1,然后對任意一個數 n 的階乘,是 n 與 n-1 階乘相乘。
所以 3 的階乘就是 3 乘以 2 的階乘,而 2 的階乘就是 2 乘以 1 的階乘,1 的階乘是 1 乘以 0 的階乘。算到一起,3 的階乘就等于 3*2*1*1,就是 6 了。
如果要給某種對象寫一個遞歸的定義,就可以用 Python 程序來實現。第一步是要來確定形式參數是什么。在這種情況下要明確階乘所用到的都應該是整形:
```py
def factorial(n):
```
如果傳來的實際參數碰巧是 0,要返回 1:
```py
def factorial(n):
if n == 0:
return 1
```
其他情況就有意思了,我們必須用遞歸的方式來調用 n-1 的階乘,然后用它來乘以 n:
```py
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result
```
這個程序的運行流程和 5.8 里面的那個倒計時有點相似。我們用 3 作為參數來調用一下這個階乘函數試試:
3 不是 0,所以分支一下,計算 n-1 的階乘。。。
2 不是 0,所以分支一下,計算 n-1 的階乘。。。
1 不是 0,所以分支一下,計算 n-1 的階乘。。。
到 0 了,就返回 1 給進行遞歸的分支。
返回值 1 與 1 相乘,結果再次返回。
返回值 1 與 2 相乘,結果再次返回。
2 的返回值再與 n 也就是 3 想成,得到的結果是 6,就成了整個流程最終得到的答案。
圖 6.1 表明了這一系列函數調用過程中的棧圖。
________________________________________

Figure 6.1: Stack diagram.
________________________________________
## 6.6 信仰之躍
跟隨著運行流程是閱讀程序的一種方法,但很快就容易弄糊涂。另外一個方法,我稱之為『思維跳躍』。當你遇到一個函數調用的時候,你不用去追蹤具體的執行流程,而是假設這個函數工作正常并且返回正確的結果。
實際上你已經聯系過這種思維跳躍了,就在你使用內置函數的時候。當你調用 math.cos 或者 math.exp 的時候,你并沒有仔細查看這些函數的函數體。你就假設他們都工作,因為寫這些內置函數的人都是很可靠的編程人員。
你調用自己寫的函數時候也是同樣道理。比如在 6.4 部分,我們謝了這個叫做 is_divisible 的函數來判斷一個數能否被另外一個數整除。一旦我們通過分析代碼和做測試來確定了這個函數沒有問題,我們就可以直接使用這個函數了,不用去理會函數體內部細節了。
對于遞歸函數而言也是同樣道理。當你進行遞歸調用的時候,并不用追蹤執行流程,你只需要假設遞歸調用正常工作,返回正確結果,然后你可以問問自己:『假設我能算出來 n-1 的階乘,我能否計算出 n 的階乘呢?』很顯然你是可以的,乘以 n 就可以了。
當然了,當你還沒寫完一個函數的時候就假設它正常工作確實有點奇怪,不過這也是我們稱之為『思維飛躍』的原因了,你總得飛躍一下。
## 6.7 斐波拉契數列
計算階乘之后,我們來看看斐波拉契數列,這是一個廣泛應用于展示遞歸定義的數學函數,[定義](http://en.wikipedia.org/wiki/Fibonacci_number 如下:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n?1) + fibonacci(n?2)
翻譯成 Python 的語言大概如下這樣:
```py
def fibonacci (n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
```
躍』的方法,如果你假設兩個遞歸調用都正常工作,整個過程就很明確了,你就得到正確答案加到一起即可。
## 6.8 檢查類型
如果我們讓階乘使用 1.5 做參數會咋樣?
```py
>>> factorial(1.5)
RuntimeError: Maximum recursion depth exceeded
```
看上去就像是無窮遞歸一樣。為什么會這樣?因為這個函數的基準條件是 n 等于 0。但如果 n 不是一個整形變量,就會無法達到基準條件,然后無窮遞歸下去。
在第一次遞歸調用的時候,傳遞的 n 值是 0.5.下一步就是-0.5.
從這開始,這個值就越來越小(就成了更小的負數了)但永遠都不會到 0.
我們有兩種選擇。我們可以嘗試著把階乘函數改寫成可以用浮點數做參數的,或者也可以讓階乘函數檢查一下參數類型。第一種選擇就會寫出一個伽瑪函數,這已經超越了本書的范圍。所以我們用第二種方法。
(譯者注:伽瑪函數(Gamma 函數),也叫歐拉第二積分,是階乘函數在實數與復數上擴展的一類函數。該函數在分析學、概率論、偏微分方程和組合數學中有重要的應用。與之有密切聯系的函數是貝塔函數,也叫第一類歐拉積分。)
我們可以用內置的 isinstance 函數來判斷參數的類型。我們也還得確定一下參數得是大于 0 的:
```py
def factorial (n):
if not isinstance(n, int):
print('Factorial is only defined for integers.')
return None
elif n < 0:
print('Factorial is not defined for negative integers.')
return None
elif n == 0:
return 1
else:
return n * factorial(n-1)
```
第一個基準條件用來處理非整數;第二個用來處理負整數。在小數或者負數做參數的時候,函數就會輸出錯誤信息,返回空到調用出來表明出錯了:
```py
>>> factorial('fred')
Factorial is only defined for integers. None
>>> factorial(-2)
Factorial is not defined for negative integers. None
```
如果兩個檢查都通過了,就能確定 n 是正整數或者 0,就可以保證遞歸能夠正確進行和終止了。
這個程序展示了一種叫做『守衛』的模式。前兩個條件就扮演了守衛的角色,避免了那些引起錯誤的變量。這些守衛能夠保證我們的代碼運行正確。
在 11.4 我們還會看到更多的靈活的處理方式,會輸出錯誤信息,并上報異常。
## 6.9 調試
把大的程序切分成小塊的函數,就自然為我們調試建立了一個個的檢查點。在不工作的函數里面,有幾種導致錯誤的可能:
* 函數接收的參數可能有錯,前置條件沒有滿足。
* 函數本身可能有錯,后置條件沒有滿足。
* 返回值或者返回值使用的方法可能有錯。
要去除第一種情況,你要在函數開頭的時候添加一個 print 語句,來輸出一下參數的值(最好加上類型)。或者你可以寫一份代碼來明確檢查一下前置條件是否滿足。
如果形式參數看上去沒問題,在每一個返回語句之前都加上 print 語句,顯示一下要返回的值。如果可以的話,盡量親自去檢查一下這些結果,自己算一算。盡量調用有值的函數,這樣檢查結果更方便些(比如在 6.2 中那樣。)
如果函數看著沒啥問題,就檢查一下函數的調用,來檢查一下返回值是不是有用到,確保返回值被正確使用。
在函數的開頭結尾添加輸出語句,能夠確保整個執行流程更加可視化。比如下面就是一個有輸出版本的階乘函數:
```py
def factorial(n):
space = ' ' * (4 * n)
print(space, 'factorial', n)
if n == 0:
print(space, 'returning 1')
return 1
else:
recurse = factorial(n-1)
result = n * recurse
print(space, 'returning', result)
return result
```
space 在這里是一串空格的字符串,是用來縮進輸出的。下面就是 4 的階乘得到的結果:
```py
factorial 4
factorial 3
factorial 2
factorial 1
factorial 0
returning 1
returning 1
returning 2
returning 6
returning 24
```
如果你對執行流程比較困惑,這種輸出會有一定幫助。有效率地進行腳手架開發是需要時間的,但稍微利用一下這種思路,反而能夠節省調試用的時間。
## 6.10 Glossary 術語列表
temporary variable:
A variable used to store an intermediate value in a complex calculation.
>臨時變量:用來在復雜運算過程中存儲一些中間值的變量。
dead code:
Part of a program that can never run, often because it appears after a return statement.
>無效代碼:一部分不會被運行的代碼,一般是書現在了返回語句之后。
incremental development:
A program development plan intended to avoid debugging by adding and testing only a small amount of code at a time.
>漸進式開發:程序開發的一種方式,每次對已有的能工作的代碼進行小規模的增補修改來降低調試的精力消耗。
scaffolding:
Code that is used during program development but is not part of the final version.
>腳手架代碼:在程序開發期間使用的代碼,但最終版本并不會包含這些代碼。
guardian:
A programming pattern that uses a conditional statement to check for and handle circumstances that might cause an error.
>守衛:一種編程模式。使用一些條件語句來檢驗和處理一些有可能導致錯誤的情況。
## 6.11 練習
### 練習 1
為下面的程序畫棧圖。程序輸出會是什么樣的?
```py
def b(z):
prod = a(z, z)
print(z, prod)
return prod
def a(x, y):
x = x + 1
return x * y
def c(x, y, z):
total = x + y + z
square = b(total)**2
return square
x = 1
y = x + 1
print(c(x, y+3, x+y))
```
### 練習 2
Ackermann 阿克曼函數的定義如下:
```py
A(m, n) = n+1 if m = 0
A(m?1, 1) if m > 0 and n = 0
A(m?1, A(m, n?1)) if m > 0 and n > 0.
```
看一下[這個連接](http://en.wikipedia.org/wiki/Ackermann_function)。寫一個叫做 ack 的函數,實現上面這個阿克曼函數。用你寫出的函數來計算 ack(3, 4),結果應該是 125.看看 m 和 n 更大一些會怎么樣。[樣例代碼](http://thinkpython2.com/code/ackermann.py).
### 練習 3
回文的詞特點是正序和倒序拼寫相同給,比如 noon 以及 redivider。用遞歸的思路來看,回文詞的收尾相同,中間部分是回文詞。
下面的函數是把字符串作為實際參數,然后返回函數的頭部、尾部以及中間字母:
```py
def first(word):
return word[0]
def last(word):
return word[-1]
def middle(word):
return word[1:-1]
```
第八章我們再看看他們是到底怎么工作的。
1. 把這些函數輸入到一個名字叫做 palindrome.py 的文件中,測試一下輸出。
如果中間你使用一個只有兩個字符的字符串會怎么樣?一個字符的怎么樣?
空字符串,比如『』沒有任何字母的,怎么樣?
2. 寫一個名叫 is_palindrome 的函數,使用字符串作為實際參數,根據字符串是否為回文詞來返回真假。機主,你可以用內置的 len 函數來檢查字符串的長度。
### 練習 4
一個數字 a 為 b 的權(power),如果 a 能夠被 b 整除,并且 a/b 是 b 的權。寫一個叫做 is_power 的函數接收 a 和 b 作為形式參數,如果 a 是 b 的權就返回真。注意:要考慮好基準條件。
### 練習 5
a 和 b 的最大公約數是指能同時將這兩個數整除而沒有余數的數當中的最大值。
找最大公約數的一種方法是觀察,如果當 r 是 a 除以 b 的余數,那么 a 和 b 的最大公約數與 b 和 r 的最大公約數相等。基準條件是 a 和 0 的最大公約數為 a。
寫一個有名叫 gcd 的函數,用 a 和 b 兩個形式參數,返回他們的最大公約數。
致謝:這個練習借鑒了 Abelson 和 Sussman 的 計算機程序結構和解譯 一書。