# 第三章 函數
在編程的語境下,“函數”這個詞的意思是對一系列語句的組合,這些語句共同完成一種運算。定義函數的時候,你要給這個函數指定一個名字,另外還好寫出這些進行運算的語句。定義完成后,就可以通過函數名來“調用”函數。
## 3.1 函數調用
此前我們已經見識過函數調用的一個例子了:
```py
>>> type(42)
<class 'int'>
```
這個函數的名字就是 tpye,括號里面的表達式叫做函數的參數。這個函數的結果是返回參數的類型。
一般來說,函數都要“傳入”一個參數,“返回”一個結果。結果也被叫做返回值。Python 提供了一些轉換數值類型的函數。比如 int 這個函數就可以把值轉換成整形,但不是什么都能轉的,遇到不能轉換的就會報錯了,如下所示:
```py
>>> int('32')
32
>>> int('Hello')
ValueError: invalid literal for int(): Hello
```
int 這個函數能把浮點數轉成整形,但不是很完美,小數部分就都給砍掉了。
```py
>>> int(3.99999)
3
>>> int(-2.3)
-2
```
float 能把整形和字符串轉變成浮點數:
```py
>>> float(32)
32.0
>>> float('3.14159')
3.14159
```
最后來看下,str 可以把參數轉變成字符串:
```py
>>> str(32)
'32'
>>> str(3.14159)
'3.14159'
```
## 3.2 數學函數
Python 內置了一個數學模塊,這一模塊提供了絕大部分常用的數學函數。模塊就是一系列相關函數的集合成的文件。
在使用模塊中的函數之前,必須先要導入這個模塊,使用導入語句:
```py
>>> import math
```
這個語句建立了一個模塊對象,名字叫做 math。如果你讓這個模塊對象顯示一下,你就會得到與之相關的信息了:
```py
>>> math
<module 'math' (built-in)>
```
模塊對象包含了一些已經定義好的函數和變量。指定模塊名和函數名,要用點(也就是英文的句號)來連接模塊名和函數名,就可以調用指定的函數了。
```py
>>> ratio = signal_power / noise_power
>>> decibels = 10 * math.log10(ratio)
>>> radians = 0.7
>>> height = math.sin(radians)
```
第一個例子用了數學的 log10 的函數,來計算信噪比的分貝值(假設信號強度和噪音強度都已知了)。數學模塊同時也提供了 log,用自然底數 e 取對數的函數。
第二個例子是對弧度值計算正弦值。通過變量名你應該能推測出正弦以及其他的三角函數(比如余弦、正切等等)都要用弧度值作為參數。所以要把角度的值從度轉換成弧度,方法就是除以 180 然后再乘以圓周率π:
```py
>>> degrees = 45
>>> radians = degrees / 180.0 * math.pi
>>> math.sin(radians)
0.707106781187
```
math.pi 這個表達式從數學模塊中得到π的一個大概精確到 15 位的近似值,存成一個浮點數。
了解了三角函數之后,你可以用試著把 2 的平方根除以二,然后對比一下這個結果和上一個結果:
```py
>>> math.sqrt(2) / 2.0
0.707106781187
```
>譯者注:畫一個三角形就知道了,45 度角兩直角邊是單位 1,斜邊必然是 2 的平方根了,對應的正弦余弦也都是這個值了。大家應該能理解吧?
## 3.3 組合
目前為止,我們已經見識了一個程序所需要的大部分元素了:變量、表達式、語句。不過咱們都是一個個單獨看到的,還沒有把它們結合起來試試。
一門編程語言最有用的功能莫過于能夠用一個個小模塊來拼接創作。例如函數的參數可以是任何一種表達式,包括代數運算符:
```py
x = math.sin(degrees / 360.0 * 2 * math.pi) ?
```
再或者函數的調用本身也可以作為參數:
```py
x = math.exp(math.log(x+1))
```
你可以在任何地方放一個值,放任何一個表達式,只有一個例外:一個聲明語句的左邊必須是變量名。任何其他的表達式放到等號左邊都會導致語法錯誤(當然也有例外,等會再給介紹)。
```py
>>> minutes = hours * 60 # right
>>> hours * 60 = minutes # wrong!
SyntaxError: can't assign to operator
```
>譯者注:上述例子里面把表達式復制為變量是不行的,所說的例外估計是指尤達大師命名法,這個后面看到再說。
## 3.4 自定義函數
目前我們學到了一些 Python 自帶的函數,自己定義新的函數也是可以的。函數定義要指定這個新函數的名字,還需要一系列語句放到這個函數里面,當調用這個函數的時候,就會運行這些語句了。
```py
def print_lyrics():
print("I'm a lumberjack, and I'm okay.")
print("I sleep all night and I work all day.")
```
這里的 def 就是一個關鍵詞,意思是這是在定義一個函數。函數的名字就是 print_lyrics,函數的命名規則和變量命名規則基本差不多,都是字幕梳子或者下劃線,但是不能用數字打頭。另外也不能用關鍵詞做函數名,還要注意盡量避免函數名和變量名發生重復。
函數名后面的括號是空的,意思是這個函數不需要參數。
函數定義的第一行叫做頭部,剩下的叫做函數體。函數頭部的末尾必須有一個冒號,函數體必須是相對函數頭部有縮進的,距離行首相對于函數頭要有四個空格的距離。函數體可以有任意長度的語句。
(譯者注:縮進是 Python 最強制的要求,本書的翻譯用的 MarkDown 在生成的時候可能未必能夠完美縮進,所以大家多注意一下自己調整哈,這個超級重要!)
在打印語句中,要打印的字符串需要用雙引號括著。單引號和雙引號效果一樣,除非是字符串中已經出現了單引號,大家一般都是用單引號的。
所有的引號都必須是鍵盤上直接是引號的那個"鍵,無論是單引號還是雙引號。就是回車鍵左邊那個。“Curly quotes”這種引號,在 Python 里面是非法的。
如果你在交互模式下面定義函數,解釋器會顯示三個小點來提醒你定義還沒有完成:
```py
>>> def print_lyrics():
...
print("I'm a lumberjack, and I'm okay.") ...
print("I sleep all night and I work all day.") ...
```
在函數定義完畢的結尾,必須輸入一行空白行。定義函數會創建一個函數類的對象,有 type 函數。
```py
>>> print(print_lyrics)
<function print_lyrics at 0xb7e99e9c>
>>> type(print_lyrics)
<class 'function'>
```
調用新函數的語法和調用內置函數是一樣的:
```py
>>> print_lyrics()
I'm a lumberjack, and I'm okay. I sleep all night and I work all day.
```
一旦你定義了一個函數,就可以在其它函數里面來調用這個函數。比如咱們重復一下剛剛討論的,寫一個叫做重 repeat_lyrics 的函數。
```py
def repeat_lyrics():
print_lyrics()
```
然后調用一下這個函數:
```py
>>> repeat_lyrics()
I'm a lumberjack, and I'm okay. I sleep all night and I work all day. I'm a lumberjack, and I'm okay. I sleep all night and I work all day.
```
當然了,實際這首歌可不是這樣的哈。
## 3.5 定義并使用
把前面這些小塊的代碼來整合一下,整體上程序看著大概是這樣的:
```py
def print_lyrics():
print("I'm a lumberjack, and I'm okay.")
print("I sleep all night and I work all day.")
def repeat_lyrics():
print_lyrics()
repeat_lyrics()
```
這個程序包含兩個函數的定義:print_lyrics 以及 repeat_lyrics,函數定義的執行就和其他語句一樣,但是效果是創建函數對象。函數定義中的語句直到函數被調用的時候才會運行,函數的定義本身不會有任何輸出。
如你所愿了,你可以建立一個函數,然后運行一下試試了。換種說法就是,在調用之前一定要先把函數定義好。
作為練習,把這個程序的最后一行放到頂部,這樣函數調用就在函數定義之前了。運行一下看看出錯的信息是什么。
然后再把函數調用放到底部,把 print_lyrics 這個函數的定義放到 repeat_lyrics 這個函數的后面。再看看這次運行會出現什么樣子?
## 3.6 運行流程
為了確保一個函數在首次被調用之前已經定義,你必須要之道語句運行的順序,也就是所謂『運行流程』。
一個 Python 程序都是從第一個語句開始運行的。從首至尾,每次運行一個語句。
函數的定義并不會改變程序的運行流程,但要注意,函數體內部的語句只有在函數被調用的時候才會運行。
函數調用就像是運行流程有了繞道的行為。沒有直接去執行下一個語句,運行流跳入到函數體內,運行里面的語句,然后再回來從離開的地方繼續執行。
這么解釋很簡明易懂了,只要你記住一個函數可以調用另一個就行。在一個函數的中間,程序有可能必須運行一下其他函數中的語句。所以運行新的函數的時候,程序可能也必須運行其他的函數!
(譯者注:看著很繞嘴,其實蠻簡單的,就是跳出跳入互相調用而已。)
幸運的是,Python 很善于追蹤應該執行的位置,所以每次一個函數執行完畢了,程序都會回到當時跳出的位置,然后繼續運行。等執行到了程序的末尾,就終止了。
總的來說,你閱讀一個程序的時候,并不一定總是要從頭到尾來讀的。有時候你要按照運行流程來讀才更好理解。
## 3.7 形式參數和實際參數
(譯者注:這里提到的形參和實參實際上是傳值方式的區別,這個在最基本的編程入門課程中老師應該都比較強調的。實際參數就是調用函數時候傳給他的那個參數;而形式參數可以理解為函數內部定義用的參數。老外對這個的思辯也很多。這里我先不說太多,翻譯著再看。
大家可以去網上多搜索一下,比如在[StackOverflow](http://stackoverflow.com/questions/1788923/parameter-vs-argument)和[MSDN](https://msdn.microsoft.com/en-us/library/9kewt1b3.aspx))
我們已經看到了一些函數了,他們都需要實際參數。比如當你調用數學的正弦函數的時候你就需要給它一個數值作為實際參數。有的函數需要一個以上的實際參數,比如冪指數函數需要兩個,一個是底數,一個是冪次。
在函數里面,實際參數會被賦值給形式參數。下面就是一個使用單個實際參數的函數的定義:
```py
def print_twice(bruce):
print(bruce)
print(bruce)
```
這個函數把傳來的實際參數的值賦給了一個名字叫做 burce 的形式參數。當函數被調用的時候,就會打印出形式參數的值兩次(無論是什么內容)。任何能打印的值都適用于這個函數。
```py
>>> print_twice('Spam')
Spam
Spam
>>> print_twice(42)
42
42
>>> print_twice(math.pi)
3.14159265359
3.14159265359
```
適用于 Python 內置函數的組合規則對自定義的函數也是適用的,所以我們可以把表達式作為實際參數:
```py
>>> print_twice('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_twice(math.cos(math.pi))
-1.0
-1.0
```
實際參數在函數被調用之前要先被運算一下,所以上面例子中作為實際參數的兩個表達式都是在 print_twice 函數調用之前僅計算了一次。
當然了,也可以用變量做實際參數了:
```py
>>> michael = 'Eric, the half a bee.'
>>> print_twice(michael)
Eric, the half a bee.
Eric, the half a bee.
```
咱們傳遞給函數的這個實際參數是一個變量,這個變量名 michael 和函數內部的形式參數 bruce 沒有任何關系。在程序主體內部參數傳過去就行了,參數名字對于函數內部沒有作用;比如在這個 print_twice 函數里面,任何傳來的值,在這個 print_twice 函數體內,都被叫做 bruce。
(譯者注:這里要跟大家解釋一下,傳遞參數的時候用的是實際參數,是把這個實際參數的值交給調用的函數,函數內部接收這個值,可以命名成任意其他名字的形式參數,差不多就這么個意思了。)
## 3.8 函數內部變量和形參都是局部的
在函數內部建立一個變量,這個變量是僅在函數體內部才存在。例如:
```py
def cat_twice(part1, part2):
cat = part1 + part2
print_twice(cat)
```
這個函數得到兩個實參,把它們連接起來,然后調用 print_twice 函數來輸出結果兩次。
```py
>>> line1 = 'Bing tiddle '
>>> line2 = 'tiddle bang.'
>>> cat_twice(line1, line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.
```
當 cat_twice 運行完畢了,這個名字叫做 cat 的變量就銷毀了。咱們再嘗試著打印它一下,就會得到異常:
```py
>>> print(cat)
NameError: name 'cat' is not defined
```
形式參數也是局部起作用的。例如在 print_twice 這個函數之外,是不存在 bruce 這個變量的。
(譯者注:當然你可以在函數外定義一個同名變量叫做 bruce,但這兩個沒有關系,大家可以動手自己試試,這也是作者所鼓勵的一種探索思維。)
## 3.9 棧圖
要追蹤一個變量能在哪些位置使用,咱們就可以畫個圖表來實現,這種圖表叫做棧圖。棧圖和我們之前提到的狀態圖有些相似,也會表征每個變量的值,不同的是棧圖還會標識出每個變量所屬的函數。
每個函數都用一個框架來表示。框架的邊上要標明函數的名字,框內填寫函數內部的形參和變量。上文中樣例代碼的棧圖如下圖 3.1 所示。

圖 3.1 棧圖
一個棧中的這些框也表示了函數調用的關系等等。在上面這個例子中,print_twice 被 cat_twice 調用了兩次,而 cat_twice 被 __main__ 這個函數調用。__main__ 這個函數很特殊,屬于最外層框架,也被叫做主函數。當你在所有函數之外建立一個變量的時候,這個變量就屬于主函數所有。
每個形式參數都保存了所對應的實際參數的值。因此 part1 的值和 line1 一樣,part2 的值就和 line2 一樣,同理可知 bruce 的值就和 cat 一樣了。
如果函數調用的時候出錯了,Python 會打印出這個出錯函數的名字,調用這個出錯函數的函數名,以及調用這個調用了出錯函數的函數的函數名,一直追溯到主函數。(譯者注:好繞口哈。。。就是會溯源回去啦。)
例如,如果你想在 print_twice 這個函數中讀取 cat 的值,就會得到一個變量名錯誤:
```py
Traceback (innermost last):
File "test.py", line 13, in __main__
cat_twice(line1, line2)
File "test.py", line 5, in cat_twice
print_twice(cat)
File "test.py", line 9, in print_twice
print(cat)
NameError: name 'cat' is not defined
```
這個一系列的函數列表,就是一個追溯了。這回告訴你哪個程序文件出了錯誤,哪一行出了錯誤,以及當時哪些函數在運行。還會告訴你引起錯誤的代碼所在行號。(譯者注:這個簡直太棒了,大家一定要留心這個功能以及出錯提示,以后要用來解決很多 bug 呢。)
追溯中對函數順序的排列是同棧圖的方框順序一樣的。當前運行的函數會放在最底部。
## 3.10 有返回值的函數 和 無返回值的函數
咱們用過的一些函數,比如數學的函數,都會返回各種結果;也沒別的好名字,就叫他們有返回值函數。其他的函數,比如 print_twice,都是進行一些操作,但不返回值。那就叫做無返回值函數好了。
當你調用一個有返回值的函數的時候,一般總是要利用一下結果的;比如,你可能需要把結果賦值給某個變量,然后在表達式里面來使用一下:
```py
x = math.cos(radians)
golden = (math.sqrt(5) + 1) / 2
```
當你在交互模式調用一個函數的時候,Python 會顯示結果:
```py
>>> math.sqrt(5)
2.2360679774997898
>>> math.sqrt(5)
2.2360679774997898
```
如果是腳本模式,你運行一個有返回值的函數,但沒有利用這個返回值,這個返回值就會永遠丟失了!(譯者注:只要有返回值就一定要利用!)
```py
math.sqrt(5)
```
這個腳本計算了 5 的平方根,但沒存儲下來,也沒有顯示出來,所以就根本沒用了。
無返回值的函數要么就是屏幕上顯示出一些內容,要么就有其他的功能,但就是沒有返回值。如果你把這種函數的結果返回給一個變量,就會的到特殊的值:空。
```py
>>> result = print_twice('Bing')
Bing Bing
>>> print(result)
None
```
這種 None 是空值的意思,和字符串'None'是不一樣的。是一種特殊的值,并且有自己的類型。(譯者注,就相當于 null 了。)
```py
>>> print(type(None))
<class 'NoneType'>
```
我們目前為止寫的函數還都是無返回值的。接下來的新的章節里面,咱們就要開始寫一些有返回值的函數了。
## 3.11 為啥要用函數?
為什么要費這么多力氣來把程序劃分成一個個函數呢?這么麻煩值得么?原因如下:
* 創建一個新的函數,你就可以把一組語句用一個名字來命名,這樣你的程序讀起來就清晰多了,后期維護調試也方便。
* 函數的出現能夠避免代碼冗余,程序內的一些重復的內容就會簡化了,變得更小巧。而且在后期進行修改的時候,你只要改函數中的一處地方就可以了,很方便。
* 把長的程序切分成一個個函數,你就可以一步步來 debug 調試,每次只應對一小部分就可以,然后把它們組合起來就可以用了。
* 精細設計的函數會對很多程序都有用處。一旦你寫好了并且除了錯,這種函數代碼可以再利用。
## 3.12 調試
給程序調試是你應當掌握的最關鍵技能之一了。盡管調試的過程會有挫敗感,也依然是最滿足智力,最有挑戰性,也是編程過程中最有趣的一個項目了。
某種程度上,調試像是偵探工作一樣。你面對著很多線索,必須推斷出導致當前結果的整個過程和事件。
調試也有點像一門實驗科學。一旦你有了一個關于所出現的錯誤的想法,你就修改一下程序再試試看。如果你的假設是正確的,你就能夠預料到修改導致的結果,這樣在編程的水平上,你就上了一層臺階了,距離讓程序工作起來也更近了。
如果你的推測是錯誤的,你必須提出新的來。就像夏洛克.福爾摩斯之處的那樣,『當你剔除了所有那些不可能,剩下的無論多么荒謬,都必然是真相。』(引自柯南道爾的小說《福爾摩斯探案:四簽名》)
對于一些人來說,編程和調試是一回事。也就是說,編程就是對一個程序逐漸進行調試,一直到程序按照設想工作為止。這種思想意味著你要從一段能工作的程序來起步,一點點做小修改和調試。
例如,Linux 是一個有上百萬行代碼的操作系統,但最早它起源于 Linus Torvalsd 的一段小代碼。這個小程序是作者用來探索 Intel 的 80386 芯片的。根據 Larry Greenfield 回憶,『Linus 早起的項目就是很小的一個程序,這個程序能夠在輸出 AAAA 和 BBBB 之間進行轉換。這后來就發展除了 Linux 了。』(引用自 Linux 用戶參考手冊 beta1 版)
## 3.13 Glossary 術語列表
function:
A named sequence of statements that performs some useful operation. Functions may or may not take arguments and may or may not produce a result.
>函數:一系列有序語句的組合,有自己的名字,并且用在某些特定用途。可以要求輸入參數,也可以沒有參數,可以返回值,也可以沒有返回值。
function definition:
A statement that creates a new function, specifying its name, parameters, and the statements it contains.
>函數定義:創建新函數的語句,確定函數的名字,形式參數,以及函數內部的語句。
function object:
A value created by a function definition. The name of the function is a variable that refers to a function object.
>函數對象:由函數定義所創建的值,函數名字指代了這一函數對象。
header:
The first line of a function definition.
>函數頭:函數定義的第一行。
body:
The sequence of statements inside a function definition.
>函數體:函數定義內部的一系列有序語句。
parameter:
A name used inside a function to refer to the value passed as an argument.
>形式參數:用來在函數內部接收實際參數傳來的值,并被函數在函數內部使用。
function call:
A statement that runs a function. It consists of the function name followed by an argument list in parentheses.
>函數調用:運行某個函數的語句。包括了函數的名字以及括號,括號內放函數需要的實際參數。
argument:
A value provided to a function when the function is called. This value is assigned to the corresponding parameter in the function.
>實際參數:當函數被調用的時候,提供給函數的值。這個值會被函數接收,賦給函數內部的形式參數。
local variable:
A variable defined inside a function. A local variable can only be used inside its function.
>局部變量:函數體內定義的變量。局部變量只在函數內部有效。
return value:
The result of a function. If a function call is used as an expression, the return value is the value of the expression.
>返回值:函數返回的結果。如果一個函數調用被用作了表達式,這個返回值就是這個表達式所代表的值。
fruitful function:
A function that returns a value.
>有返回值函數:返回一個值作為返回值的函數。
void function:
A function that always returns None.
>無返回值函數:不返回值,只返回一個空 None 的函數。
None:
A special value returned by void functions.
>空值:無返回值函數所返回的一種特殊的值。
module:
A file that contains a collection of related functions and other definitions.
>模塊:包含一系列相關函數以及其他一些定義的文件。
import statement:
A statement that reads a module file and creates a module object.
>導入語句:讀取模塊并且創建一個模塊對象的語句。
module object:
A value created by an import statement that provides access to the values defined in a module.
>模塊對象:導入語句創建的一個值,允許訪問模塊所定義的值。
dot notation:
The syntax for calling a function in another module by specifying the module name followed by a dot (period) and the function name.
>點符號:調用某一個模塊的某一函數的語法形式,就是模塊名后加一個點,也就是英文的句號,再加函數名。
composition:
Using an expression as part of a larger expression, or a statement as part of a larger statement.
>組合:把表達式作為更大的表達式的一部分,或者把語句作為更大語句的一部分。
flow of execution:
The order statements run in.
>運行流程:語句運行的先后次序。
stack diagram:
A graphical representation of a stack of functions, their variables, and the values they refer to.
>棧圖:對函數關系、變量內容及結構的圖形化表示。
frame:
A box in a stack diagram that represents a function call. It contains the local variables and parameters of the function.
>框架:棧圖中的方框,表示了一次函數調用。包括函數的局部變量和形式參數。
traceback:
A list of the functions that are executing, printed when an exception occurs.
>追蹤:對運行中函數的列表,當有異常的時候就會輸出。
## 3.14 練習
### 練習 1
寫一個名叫 right_justify 的函數,形式參數是名為 s 的字符串,將字符串打印,前面流出足夠的空格,讓字符串最后一個字幕在第 70 列顯示。
```py
>>> right_justify('monty') monty
```
提示:使用字符拼接和重復來實現。另外 Python 還提供了內置的名字叫做 len 的函數,可以返回一個字符串的長度,比如 len('monty')的值就是 5 了。
### 練習 2
你可以把一個函數對象作為一個值賦給一個變量或者作為一個實際參數來傳遞給其他函數。比如,do_twice 就是一個把其他函數對象當做參數的函數,它的功能是調用對象函數兩次:
```py
def do_twice(f):
f()
f()
```
下面是另一個例子,這里用了 do_twice 來調用一個名叫 print_spam 的函數兩次。
```py
def print_spam():
print('spam')
do_twice(print_spam)
```
1.把上面的例子寫成腳本然后試一下。
2.修改一下 do_twice 這個函數,讓它接收兩個實際參數,一個是函數對象,一個是值,調用對象函數兩次,并且賦這個值給對象函數作為實際參數。
3.把 print_twice 這個函數的定義復制到你的腳本里面,去本章開頭找一下這個例子哈。
4.用修改過的這個 do_twice 來調用 print_twice 兩次,用字符串『spam』傳遞過去作為實際參數。
5.定義一個新的函數,名字叫做 do_four,使用一個函數對象和一個值作為實際參數,調用這個對象函數四次,傳遞這個值作過去為對象函數的一個形式參數。這個函數體內只要有兩個語句就夠了,而不是四個。
[樣例代碼](http://thinkpython2.com/code/do_four.py):
### 3 練習三
注意:這個練習應該只用咱們目前學習過的語句和其他功能來實現。
1.寫一個函數,輸出如下:

提示:要一次打印超過一行,可以用逗號分隔一下就能換行了。如下所示:
```py
print('+', '-')
```
默認情況下,print 會打印到下一行,你可以手動覆蓋掉這個行為,在末尾輸出一個空格就可以了:
```py
print('+', end=' ')
print('-')
```
上面的語句輸出結果就是:'+ -'。
沒有參數的 print 語句會把當前的行結束,去下一行。
2.寫一個四行四列的小網格繪制的程序。
[樣例](http://thinkpython2.com/code/grid.py)
此練習基于 Oualline 的書《實踐 C 語言編程》第三版,O'Reilly 出版社,1997 年版