National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 22

函式與模組化

把程式切成小塊:參數、回傳值與模組
Interaction實作 25 minute(s)

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: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 依序往外找:

  1. Local:這個函式自己的區域
  2. Enclosing:外層函式的區域(函式裡面又定義函式時才有)
  3. Global:整個檔案(模組)的全域
  4. Built-in:Python 內建的名字,例如 print、len

在某一層找到就停下,不會再往上找;四層都找不到就出現 NameError。 所以函式裡指派一個和全域變數同名的變數,建立的是另一個區域變數,不會改到外面那個。

這樣設計的好處是:每個函式像一個獨立的房間,裡面用什麼名字都不會干擾到別的函式。寫大型程式時,不必擔心自己取的變數名稱和同事撞名。

互動練習:名稱一層一層往外找

上方的程式執行到 add(10, 20) 裡面時,點一個名稱,看 Python 從 L 層開始一層一層往上找。點 total:函式裡找到的是區域的 30,還是全域的 0?點 len:要找到第幾層?點 spam:四層都沒有會怎樣?

想一想:參數、回傳值與堆疊

1.

下面的程式會印出什麼?

def double(x):
    print(x * 2)

r = double(5)
print(r)
2.

定義 def area(w, h):,呼叫 area(3, 4)。下列敘述何者正確?

3.

主程式呼叫 main(),main 呼叫 sum_of_squares,sum_of_squares 再呼叫 square。正在執行 square 時,呼叫堆疊由下往上是?

第 1 次作答

模組化:把大程式切成小塊

寫一支大程式時,很少從第一行一路寫到最後一行。常見的做法是由上而下拆解:先想「整件事要做什麼」, 再把它拆成幾個小工作,每個小工作寫成一個函式,最後由一個函式把它們組合起來。 下面的動畫把「印出成績報告」拆成「算平均」「換成等級」兩個小函式,再由 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 模組裡現成的函式(無條件進位),它的內部不會顯示在動畫裡,就像使用的人不必知道它怎麼寫。

想一想:作用範圍與模組化

1.

下面的程式會印出什麼?

x = 10

def g():
    x = 99
    return x + 1

y = g()
print(x, y)
2.

下列哪一項不是把程式切成函式與模組的好處?

第 1 次作答

實作:判斷閏年的函式

完成函式 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 軌的函式單元。

這一課完成了!
Next lesson:物件導向概念
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。