Learning Objectives
- 說明函式是有名字、可以重複使用的一段程式,由輸入(參數)、處理、輸出(回傳值)組成
- 分辨參數與引數,分辨 return(交回結果)與 print(印在螢幕上)
- 用呼叫堆疊說明函式呼叫時發生的事:每次呼叫有自己的框架,結束後回到呼叫的位置
- 分辨區域變數與全域變數,說明名稱依什麼順序查找
- 說明模組化的好處,會用 import 使用模組裡的函式
同一段公式要寫幾次
假設你在寫一支天氣 App,要把攝氏溫度換成華氏:華氏 = 攝氏 × 9 / 5 + 32。
今天的溫度要換、明天的要換、一週七天都要換。如果每次都把公式複製貼上一次,會有兩個麻煩:
- 程式變得很長,讀的人看不出「這幾行其實在做同一件事」
- 公式寫錯時,要找出每一個複製的地方逐一修改,漏改一處就會出錯
解法是把這段公式包起來、取個名字,需要時用名字「呼叫」它。這就是函式(function),有些書也叫函數或副程式。 第 02 課說演算法是「輸入、處理、輸出」;函式就是把一個小演算法包成一個有名字的黑盒子:
輸入(參數) 處理 輸出(回傳值)
25 ──▶ c × 9 / 5 + 32 ──▶ 77.0
使用的人只要知道「放什麼進去、會拿到什麼」,不必知道裡面怎麼算。
定義函式與呼叫函式
使用函式分成兩步:先定義(告訴電腦這個函式做什麼),再呼叫(真的叫它做事)。
def c_to_f(c): # 定義:函式名稱 c_to_f,參數 c
f = c * 9 / 5 + 32 # 處理
return f # 輸出:把結果交回呼叫的地方
a = c_to_f(25) # 呼叫:引數 25 交給參數 c,回傳值放進 a
| 名詞 | 在哪裡 | 例子 |
|---|---|---|
| 參數(parameter) | 定義時括號裡的名字,像是「空格」 | c |
| 引數(argument) | 呼叫時括號裡實際的值,用來「填空格」 | 25 |
| 回傳值(return value) | return 後面的值,交回呼叫的地方 |
77.0 |
return 不等於 print:
return才是把結果交回去,讓呼叫的地方可以存進變數、再拿來計算。 函式沒有寫return時,回傳的是代表「沒有值」的None。
定義一次,呼叫三次
注意兩件事:第 2 行的 def 只是建立函式,裡面的程式這時不會執行;每次呼叫時,右邊會多出一個 c_to_f 的框架,裡面有這次呼叫自己的 c 和 f,return 之後框架就消失。
呼叫堆疊:函式呼叫函式
函式裡面還可以呼叫別的函式。電腦怎麼記得「做完之後要回到哪裡」?答案是呼叫堆疊(call stack)。
每呼叫一次函式,電腦就在堆疊最上面放一個框架(frame),裡面記著這次呼叫的參數、區域變數,以及做完要回到哪一行。
函式 return 時,最上面的框架被拿掉,程式回到下面那個框架停下的地方繼續。
呼叫 square(3) 時的堆疊 square 回傳之後
┌────────────────────┐
│ square x=3 │ ← 正在執行
├────────────────────┤ ┌────────────────────┐
│ sum_of_squares a=3 │ │ sum_of_squares a=3 │ ← 回到這裡
├────────────────────┤ ├────────────────────┤
│ main │ │ main │
├────────────────────┤ ├────────────────────┤
│ 主程式 │ │ 主程式 │
└────────────────────┘ └────────────────────┘
就像疊盤子:最後放上去的最先拿走(後進先出)。第 25 課會再介紹「堆疊」這種資料結構;呼叫堆疊就是它最重要的應用之一。
函式一層呼叫一層、一直沒有結束時,堆疊的空間會被用完,這就是常聽到的堆疊溢位(stack overflow);Python 會在那之前擋下來,回報 RecursionError。
看呼叫堆疊長高再縮回去
右邊的框架由上往下排,最下面是正在執行的函式。看它從「主程式」長到 main、sum_of_squares、square 共四層,每次 return 少一層。square 被呼叫了兩次,每次都是新的框架,x 各自獨立。
區域變數與全域變數
在函式裡建立的變數是區域變數(local variable):它放在這次呼叫的框架裡,函式結束就跟著消失,外面看不到。 在所有函式外面建立的是全域變數(global variable),整支程式都看得到。
函式裡用到一個名字時,Python 依序往外找:
- Local:這個函式自己的區域
- Enclosing:外層函式的區域(函式裡面又定義函式時才有)
- Global:整個檔案(模組)的全域
- Built-in:Python 內建的名字,例如
print、len
在某一層找到就停下,不會再往上找;四層都找不到就出現 NameError。
所以函式裡指派一個和全域變數同名的變數,建立的是另一個區域變數,不會改到外面那個。
這樣設計的好處是:每個函式像一個獨立的房間,裡面用什麼名字都不會干擾到別的函式。寫大型程式時,不必擔心自己取的變數名稱和同事撞名。
想一想:參數、回傳值與堆疊
下面的程式會印出什麼?
def double(x):
print(x * 2)
r = double(5)
print(r)
定義 def area(w, h):,呼叫 area(3, 4)。下列敘述何者正確?
主程式呼叫 main(),main 呼叫 sum_of_squares,sum_of_squares 再呼叫 square。正在執行 square 時,呼叫堆疊由下往上是?
模組化:把大程式切成小塊
寫一支大程式時,很少從第一行一路寫到最後一行。常見的做法是由上而下拆解:先想「整件事要做什麼」,
再把它拆成幾個小工作,每個小工作寫成一個函式,最後由一個函式把它們組合起來。
下面的動畫把「印出成績報告」拆成「算平均」「換成等級」兩個小函式,再由 report 組合。
一個函式包幾行程式;相關的函式再放進同一個檔案,就是一個模組(module)。這種組織程式的方式叫模組化(modular programming)。
| 好處 | 說明 |
|---|---|
| 重複使用 | 寫好一次,到處都能呼叫,別的程式也能匯入使用 |
| 容易修改 | 規則改了(例如等級的分數線)只要改一個函式 |
| 分工合作 | 每個人負責不同的模組,只要先約定好函式的名稱、參數與回傳值 |
| 容易測試 | 每個函式可以單獨拿來測試,確定它對了再組合 |
| 隱藏細節 | 使用的人只需要知道「怎麼呼叫」,不必知道裡面怎麼做(這叫抽象化) |
程式語言通常附帶一大組現成的模組,叫做標準函式庫(standard library):Python 的 math 有數學函式、random 產生亂數、datetime 處理日期。
用 import 模組名稱 匯入後,以 模組名稱.函式名稱(...) 呼叫。自己寫的模組也一樣:
# geometry.py(自己寫的模組)
PI = 3.14159
def circle_area(r):
return round(PI * r * r, 2)
# main.py(放在同一個資料夾)
import geometry
print(geometry.circle_area(2)) # 印出 12.57
第一次 import geometry 時,Python 會找到 geometry.py 並從頭執行一次,之後就能用 geometry. 開頭呼叫裡面的函式。
把大問題拆成小函式
主程式只有最後兩行,每位同學呼叫一次 report。看呼叫堆疊:report 先呼叫 average 拿到平均,再呼叫 letter 換成等級;math.ceil 是標準函式庫 math 模組裡現成的函式(無條件進位),它的內部不會顯示在動畫裡,就像使用的人不必知道它怎麼寫。
想一想:作用範圍與模組化
下面的程式會印出什麼?
x = 10
def g():
x = 99
return x + 1
y = g()
print(x, y)
下列哪一項不是把程式切成函式與模組的好處?
實作:判斷閏年的函式
完成函式 is_leap(year):是閏年就回傳 True,否則回傳 False。現行曆法的規則是:能被 4 整除但不能被 100 整除,或能被 400 整除,就是閏年。下面的迴圈會用四個年份呼叫你的函式。year % 4 == 0 可以判斷 year 能不能被 4 整除(% 是取餘數)。
重點整理
- 函式是有名字、可重複使用的一段程式:參數是輸入,
return交回輸出。先定義(def),再呼叫。 - 參數是定義時的名字,引數是呼叫時給的值。
print只印在螢幕上,return才把結果交回;沒寫return會得到None。 - 呼叫堆疊:每次呼叫放上一個框架,
return時拿掉,回到下面那層停下的地方;後進先出。 - 區域變數只活在一次呼叫裡;名稱依 Local、Enclosing、Global、Built-in 的順序查找。
- 模組化:大問題拆成函式,相關函式放進模組;好處是重複使用、容易修改、分工、測試與隱藏細節。
下一課:函式把「動作」包起來;物件導向更進一步,把「資料」和「操作這些資料的動作」包在一起。
想多練習函式的參數、預設值與作用範圍,可以接著上 Python 軌的函式單元。