1 函數調用
在程式設計中,函數是指用於進行某種計算的一系列語句的有名稱的組合。定義一個函數時,需要指定函數的名稱並寫下一系列程式語句。之後,就可以使用名稱來“調用”這個函數。前面我們已經見過函數調用的例子:
>>> type(32)
<type 'int'>
這個函數的名稱是 type,括弧中的運算式我們稱之為函數的參數。這個函數調用的結果是參數的類型。
我們通常說函數“接收”參數,並“返回”結果。這個結果稱為傳回值(return value)。
2 類型轉換函式
Python提供了一些可將某個值從一種類型轉換為另一種類型的內建函數。int函數可以把任何可以轉換為整型的值轉換為整型;如果轉換失敗,則會報錯:
>>> int('32')
32
>>> int('Hello')
ValueError: invalid literal for int(): Hello
int可以將浮點數轉換為整數,但不會做四捨五入操作,而是直接捨棄小數部分。
>>> int(3.99999)
3
>>> int(-2.3)
-2
float函數將整數和字串轉換為浮點數:
>>> float(32)
32.0
>>> float('3.14159')
3.14159
最後,str函數將參數轉換為字串:
>>> str(32)
'32'
>>> str(3.14159)
'3.14159'
3 數學函數
Python有一個數學計算模組,提供了大多數常用的數學函數。模組是指包含一組相關的函數的檔案。
要想使用一個模組,需要先將它匯入(import)運行環境:
>>> import math
這個語句將會建立一個名為math的模組對象(module object)。如果列印這個對象,可以看到它的一些資訊:
>>> print math
<module 'math' (built-in)>
模組對象包含了這個模組中定義的函數和變數。若要訪問其中的一個函數,需要同時指定模組名稱和函數名稱,用一個句點(.)分隔。這個格式稱為句點標記法(dot notation)。
>>> ratio = signal_power / noise_power
>>> decibels = 10 * math.log10(ratio)
>>> radians = 0.7
>>> height = math.sin(radians)
上面第一個例子使用了log10來計算以分貝為單位的訊號/雜訊比(假設signal_power和noise_power都已經事先定義好了)。math模組也提供了log函數,用來計算底為e的自然對數。
第二個例子計算radians的正弦值。這個變數名已經暗示了,sin以及cos、tan等三角函數接受的參數是以弧度(radians)為單位的。若要將角度轉換為弧度,可以除以360再乘以2π:
>>> degrees = 45
>>> radians = degrees / 360.0 * 2 * math.pi
>>> math.sin(radians)
0.707106781187
運算式math.pi從math模組中獲得變數pi。這個變數的值是π的近似值,大約精確到15位元字。
如果你瞭解三角函數,可以把上面的結果和2的平方根的一半進行比較:
>>> math.sqrt(2) / 2.0
0.707106781187
4 組合
到現在為止,我們已經分別瞭解了程式的基本元素——變數、運算式和語句,但還沒有接觸如何將它們有機地組合起來。
程式設計語言最有用的特性之一就是可以將各種小的構建塊(building block)組合起來。比如,函數的參數可以是任何類型的運算式,包括算術符號:
x = math.sin(degrees / 360.0 * 2 * math.pi)
甚至還包括函數調用:
x = math.exp(math.log(x+1))
基本上,在任何可以使用值的地方,都可以使用任意運算式,只有一個例外:賦值運算式的左邊必須是變數名稱,在左邊放置任何其他的運算式都是語法錯誤(後面我們還會看到這條規則的例外情況)。
>>> minutes = hours * 60 # 正確
>>> hours * 60 = minutes # 錯誤!
SyntaxError: can't assign to operator
5 添加新函數
至此,我們都只是在使用Python提供的函數,其實我們也可以自己添加新的函數。函數定義指定新函數的名稱,並提供一系列程式語句。當函數被調用時,這些語句會順序執行。
下面是一個例子:
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。函數名稱的書寫規則和變數名稱一樣:字母、數字和某些標點是合法的,但第一個字元不能是數字。關鍵字不能作為函數名,而且我們應盡量避免函數和變數同名。
函數名後的空括弧表示它不接收任何參數。
函數定義的第一行稱為函數頭(header),其他部分稱為函數體(body)。函數頭應該以冒號結束,函數體整體縮排一級。依照慣例,縮排總是使用4個空格,參看3.14節。函數體的代碼語句行數不限。
本例中print語句裡的字串使用雙引號括起來。單引號和雙引號的作用相同。大部分情況下,人們都使用單引號,只在本例中這樣的特殊情況下才使用雙引號。本例中的字串裡本身就存在單引號(單引號也作為省略符號用,如I'm)。
如果在互動模式裡輸入函數定義,則解譯器會輸出省略符號(...)提示你當前的定義還沒有結束:
>>> def print_lyrics():
... print "I'm a lumberjack, and I'm okay."
... print "I sleep all night and I work all day."
...
想要結束這個函數的定義,需要輸入一個空行(在指令檔中則不需要如此)。
定義一個函數後,會建立一個同名的變數。
>>> print print_lyrics
<function print_lyrics at 0xb7e99e9c>
>>> type(print_lyrics)
<type 'function'>
變數print_lyrics的值是一個函數對象,其類型是'function'。
調用新建立的函數的方式,與調用內建函數是一樣的:
>>> print_lyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
定義好一個函數之後,就可以在其他函數中調用它。比如,若想重複上面的歌詞,我們可以寫一個repeat_lyrics函數:
def repeat_lyrics():
print_lyrics()
print_lyrics()
然後可以調用repeat_lyrics:
>>> 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.
當然,這首歌其實並不是這麼唱的。
6 定義和使用
將前面一節零散的代碼整合起來,整個程式就像下面這個樣子:
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()
print_lyrics()
repeat_lyrics()
這個程式包含兩個函數定義:print_lyrics和repeat_lyrics。(在解譯器執行程式代碼時)函數定義的執行方式和其他語句一樣,不同的是執行後會建立函數對象。函數體裡面的語句並不會立即執行,而是等到函數被調用時才執行。函數定義不會產生任何輸出。
你可能已經猜到,必須先建立一個函數,才能執行它。換言之,函數定義必須在函數的第一次調用之前先執行。
練習1
將程式的最後一行移動到首行,於是函數調用會先於函數定義執行。運行程式並查看會有什麼樣的錯誤資訊。
練習2
將函數調用那一行放回到末尾,並將函數print_lyrics的定義放到函數repeat_lyrics定義之後。這時候運行程式會發生什嗎?
7 執行流程
為了保證函數的定義先於其首次調用執行,你需要知道程式中語句執行的順序,即執行流程。
執行總是從程式的第一行開始。從上到下,按順序,每次執行一條語句。
函數定義並不會改變程式的執行流程,但應注意函數體中的語句並不立即執行,而是等到函數被調用時執行。
函數調用可以看作程式執行流程中的一個迂迴路徑。遇到函數調用時,並不會直接繼續執行下一條語句,而是跳到函數體的第一行,繼續執行完函數體的所有語句,再跳回到原來離開的地方。
這樣看似簡單,但馬上你會發現,函數體中可以調用其他函數。當程式流程運行到一個函數之中時,可能需要執行其他函數中的語句。但當執行那個函數中的語句時,又可能再需要調用執行另一個函數的語句!
幸好Python對於它運行到哪裡有很好的記錄,所以每個函數執行結束後,程式都能跳回到它離開的地方。直到執行到整個程式的結尾,才會結束程式。
前面這段枯燥的描述,寓意何在?當你閱讀代碼時,並不總是應該一行行按照書寫順序閱讀。有時候,按照執行的流程來閱讀代碼,可能理解效果更好。
8 形參和實參①
我們已經看到,有些內建函數需要傳入參數。比如,當調用math.sin時,需要傳入一個數字作為實參。有的函數需要多個實參:math.pow需要兩個,分別是基數(base)和指數(exponent)。
在函數內部,實參會被賦值給形參。下面的例子是一個使用者自訂的函數,接收一個實參:
def print_twice(bruce):
print bruce
print bruce
這個函數在調用時會把實參的值賦到形參bruce上,並將其列印兩次。
這個函數對任何可以列印的值都可用。
>>> print_twice('Spam')
Spam
Spam
>>> print_twice(17)
17
17
>>> print_twice(math.pi)
3.14159265359
3.14159265359
內建函數的組合規則,在使用者自訂函數上也同樣可用,所以我們可以對print_twice使用任何錶達式作為實參:
>>> print_twice('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_twice(math.cos(math.pi))
-1.0
-1.0
作為實參的運算式會在函數調用之前先執行。所以在這個例子中,運算式'Spam'*4和math.cos(math.pi)都只執行一次。
你也可以使用變數作為實參:
>>> michael = 'Eric, the half a bee.'
>>> print_twice(michael)
Eric, the half a bee.
Eric, the half a bee.
作為實參傳入到函數的變數的名稱(michael)和函數定義裡形參的名稱(bruce)沒有關係。函數內部只關心形參的值,而不用關心它在調用前叫什麼名字;在print_twice函數內部,大家都叫bruce。
9 變數和形參是局部的
當你在函數體內建立一個變數時,它是局部的(local),即它只存在於這個函數之內。比如:
def cat_twice(part1, part2):
cat = part1 + part2
print_twice(cat)
這個函數接收兩個實參,將它們拼接起來,並將結果列印兩遍。下面是一個使用這一函數的例子:
>>> line1 = 'Bing tiddle '
>>> line2 = 'tiddle bang.'
>>> cat_twice(line1, line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.
當cat_twice結束時,變數cat會被銷毀。這時再嘗試列印它的話,會得到一個異常:
>>> print cat
NameError: name 'cat' is not defined
形參也是局部的。比如,在print_twicebruce這個變數。
10 棧圖
要跟蹤哪些變數在哪些地方使用,有時候畫一個棧圖(stack diagram)會很方便。和狀態圖一樣,棧圖可以展示每個變數的值,不同的是它會展示每個變數所屬的函數。
每個函數使用一個幀包含,幀在棧圖中就是一個帶著函數名稱的盒子,裡面有函數的參數和變數。前面的函數樣本的棧圖如圖1所示。
圖1 棧圖
圖中各個幀從上到下安排成一個棧,能夠展示出哪個函數被哪個函數調用了。在這個例子裡,printtwice被cattwice調用,而cattwice被main調用。_main是用於表示整個棧圖的圖框的特別名稱。當你在所有函數之外建立變數時,它就是屬於__main的。
每個形參都指向與其對應的實參相同的值,所以,part1和line1的值相同,part2和line2的值相同,而bruce和cat的值相同。
如果調用函數的過程中發生了錯誤,Python會列印出函數名,以及調用它的函數的名稱,以及調用這個調用者的函數名,依此類推,一直到main。
比如,如果你在print_twice中訪問cat變數,則會得到一個NameError:
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
上面這個函數列表被稱為回溯(traceback)。它告訴你錯誤出現在哪個程式檔案,哪一行,以及哪些函數正在運行。它也會顯示導致錯誤的那一行代碼。
回溯中函數的順序和棧圖中圖框的順序一致。當前正在執行的函數列在最底部。
11 有傳回值函數和無傳回值函數
我們使用過的函數中,有一部分函數,如數學函數,會產生結果。因為沒有想到更好的名字,我稱這類函數為有傳回值函數(fruitful function)。另一些函數,如print_twice,會執行一個動作,但不返回任何值。我們稱這類函數為無傳回值函數(void function)。
當你調用一個有傳回值的函數時,大部分情況下都想要對結果做某種操作。比如,你可能會想把它賦值給一個變數,或者用在一個運算式中:
x = math.cos(radians)
golden = (math.sqrt(5) + 1) / 2
在互動模式中調用函數時,Python會直接顯示結果:
>>> math.sqrt(5)
2.2360679774997898
但是在指令碼中,如果只是直接調用這類函數,那麼它的傳回值就會永遠丟失掉!
math.sqrt(5)
這個指令碼計算5的平方根,但由於並沒有把計算結果儲存到某個變數中,或顯示出來,所以其實沒什麼實際作用。
無傳回值函數可能在螢幕上顯示某些東西,或者有其他的效果,但是它們沒有傳回值。如果你試著把它們的結果賦值給某個變數,則會得到一個特殊的值None。
>>> result = print_twice('Bing')
Bing
Bing
>>> print result
None
值None和字串'None'並不一樣。它是一個特殊的值,有自己獨特的類型:
>>> print type(None)
<type 'NoneType'>
到目前為止,我們自訂的函數都是無傳回值函數。
12 為什麼要有函數
為什麼要花功夫將程式拆分成函數呢?也許剛開始編程的時候這其中的原因並不明晰。下面這些解釋都可作為參考。
建立一個函數,可以讓你有機會給一組語句命名,這樣可以讓代碼更易讀和更易調試。
函數可以通過減少重複代碼使程式更短小。後面如果你需要修改代碼,也只要修改一個地方即可。
將一長段程式拆分成幾個函數後,可以對每一個函數單獨進行調試,再將它們組裝起來成為完整的產品。
一個設計良好的函數,可以在很多程式中使用。書寫一次,調試一次,複用無窮。
13 使用from匯入模組
Python提供了兩種匯入模組的方式;我們已經見過其中一種:
>>> import math
>>> print math
<module 'math' (built-in)>
>>> print math.pi
3.14159265359
如果你匯入math,則會得到名為math的模組對象。模組對象包含了pi這樣的常量以及諸如sin和exp這樣的函數。
但是如果直接存取pi,則會發生錯誤。
>>> print pi
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'pi' is not defined
這時候,你可以像下面這樣來匯入模組中的某個對象:
>>> from math import pi
現在就可以直接存取pi,而不需要使用句點標記法math.pi了。
>>> print pi
3.14159265359
或者,也可以使用星號來匯入一個模組的所有成員:
>>> from math import *
>>> cos(pi)
-1.0
用這種方式匯入模組內所有的成員,好處是可以使你的代碼更簡潔,但缺點是不同模組的同名成員之間,或者和自訂的變數之間,可能發生名字衝突。
14 調試
如果你使用文字編輯器來編寫指令碼,則可能會遇到縮排時空格和定位字元混淆的問題。避免這種問題的最好辦法是只使用空格(不用定位字元)。大部分識別Python的文字編輯器都預設這麼處理,不過也有一些不支援。
定位字元和空格都是不可見的,因而會很難調試,所以應嘗試找一個能幫你處理縮排的編輯器。
另外,不要忘了在運行程式前儲存它。有的開發環境會自動儲存,但也有不自動儲存的。如果不儲存,則你寫好的代碼和啟動並執行代碼並不一樣。
如果啟動並執行報錯的代碼和你寫的不一樣,調試時會浪費很多時間!
所以一定要確保你眼前所看的代碼和所啟動並執行代碼是一致的。如果不確定,可以在程式開頭寫一句print 'hello'並再運行一次。如果沒有看到hello輸出,則你啟動並執行不是正確的程式!
15 術語表
函數(function):一個有名稱的語句序列,可以進行某種有用的操作。函數可以接收或者不接收參數,可以返回或不返回結果。
函數定義(function definition):一個用來建立新函數的語句,指定函數的名稱、參數以及它執行的語句序列。
函數對象(function object):函數定義所建立的值。函數名可以用作變數來引用一個函數對象。
函數頭(header):函數定義的第一行。
函數體(body):函數定義內的語句序列。
形參(parameter):函數內使用的用來引用作為實參傳入的值的名稱。
函數調用(function call):執行一個函數的語句。它由函數名稱和參數列表組成。
實參(argument):當函數調用時,提供給它的值。這個值會被賦值給對應的形參。
局部變數(local variable):函數內定義的變數。局部變數只能在函數體內使用。
傳回值(return value):函數的結果。如果函數被當做運算式調用,傳回值就是運算式的值。
有傳回值函數(fruitful function):返回一個值的函數。
無傳回值函數(void function):沒有傳回值的函數。
模組(module):一個包含相關函數以及其他定義的集合的檔案。
import語句(import statement):讀入一個模組檔案,並建立一個模組對象的語句。
模組對象(module object):使用import語句時建立的對象,提供對模組中定義的值的訪問。
句點標記法(dot notation):調用另一個模組中的函數的文法,使用模組名加上一個句點符號,再加上函數名。
組合(composition):使用一個運算式作為更大的運算式的一部分,或者使用語句作為更大的語句的一部分。
執行流程(flow of execution):程式運行中語句執行的順序。
棧圖(stack diagram):函數棧的圖形表達形式,也展示它們的變數,以及這些變數引用的值。
圖框(frame):棧圖中的一個圖框,表達一個函數調用。它包含了局部變數以及函數的參數。
回溯(traceback):當異常發生時,列印出正在執行的函數棧。
16 練習
練習3 Python提供了一個內建函數len,返回一個字串的長度。所以len('allen')的值是5。
編寫一個函數right_justify,接收一個字串形參s,並列印出足夠的前置空白,以達到最後一個字元顯示在第70列上。
>>> right_justify('allen')
allen
練習4 函數對象是一個值,你可以將它賦值給變數,或者作為實參傳遞。例如do_twice是一個函數,接收一個函數對象作為實參,並調用它兩次:
def do_twice(f):
f()
f()
下面是一個使用do_twice來調用一個print_spam函數兩次的樣本:
def print_spam():
print 'spam'
do_twice(print_spam)
1.將這個樣本存入指令碼中並測試它。
2.修改do_twice,讓它接收兩個實參,一個是函數對象,另一個是一個值,它會調用函數對象兩次,並傳入那個值作為實參。
3.編寫一個更通用的print_spam,叫做print_twice,接收一個字串形參,並列印它兩次。
4.使用修改版的do_twice來調用print_twice兩次,並傳入實參'spam'。
5.定義一個新的函數do_four,接收一個函數對象與一個值,使用這個值作為實參調用函數4次。這個函數的函數體應該只有兩個語句,而不是四個。
練習5 這個練習可以只用語句和我們已經學過的其他語言特性實現。
1.編寫一個函數,繪製如下的表格:
提示
:
要在同一行列印多個值,你可以使用逗號分隔不同的值:
print '+','-'
如果值序列的結尾有一個逗號,Python不會換行,所以後面的列印語句會出現在同一行。
print '+',
print '-'
這兩個語句的輸出是'+ -'。 2.編寫一個函數繪製類似的表格,但有4行4列。