Python 課程 – 函式(Functions)
主題:函式作為一級公民(First‑Class Function)
簡介
在許多程式語言中,函式(function)不僅僅是執行程式碼的工具,它本身也是一個可以被操作的物件。Python 把函式視為「一級公民」——也就是說,函式可以像變數、數字、字串一樣被 指派、傳遞、儲存、返回,甚至可以動態產生新的函式。
掌握這個概念不只是理論上的好玩,它直接影響到我們寫出 可重用、可組合、可測試 的程式碼。從簡單的回呼(callback)到裝飾器(decorator)或函式式編程(functional programming)風格,所有進階技巧的基礎都建立在「函式是一等公民」之上。
本篇文章會以 淺顯易懂 的方式說明什麼是一級公民函式、它的核心特性、常見的實作模式,以及在真實專案中如何善加利用。
核心概念
1. 函式可以被指派給變數
在 Python 中,函式本身是一個 function 物件。只要把函式名稱(不加括號)寫在等號右側,就會把這個函式物件指派給左側的變數。
def greet(name: str) -> str:
"""傳回問候字串"""
return f"哈囉,{name}!"
# 把函式指派給另一個變數
say_hello = greet
# 兩個變數實際指向同一個函式物件
print(say_hello("小明")) # => 哈囉,小明!
print(greet("小美")) # => 哈囉,小美!
重點:
greet與say_hello皆是指向同一個函式物件,呼叫方式完全相同。
2. 函式可以作為參數傳遞(高階函式)
所謂「高階函式」是指 接受函式作為參數 或 返回函式 的函式。最常見的情境是「回呼」──把一段行為交給另一段程式去執行。
from typing import Callable, List
def apply_to_each(items: List[int], func: Callable[[int], int]) -> List[int]:
"""將 func 套用到每個元素,回傳新列表"""
return [func(item) for item in items]
def square(x: int) -> int:
return x * x
numbers = [1, 2, 3, 4]
squared = apply_to_each(numbers, square) # 把 square 函式傳入
print(squared) # => [1, 4, 9, 16]
技巧:使用
typing.Callable可以為傳入的函式加上型別提示,提升可讀性與 IDE 的自動補完功能。
3. 函式可以作為返回值(閉包)
函式不僅能被傳入,還能 被其他函式產生並返回。這種模式常用來建立「閉包」──即返回的函式會捕獲外層函式的環境變數。
def make_multiplier(factor: int):
"""產生一個乘以 factor 的函式"""
def multiplier(x: int) -> int:
return x * factor # factor 來自外層作用域
return multiplier
times_three = make_multiplier(3) # 產生乘以 3 的函式
times_five = make_multiplier(5) # 產生乘以 5 的函式
print(times_three(10)) # => 30
print(times_five(10)) # => 50
說明:
multiplier內部使用了外層的factor,即使make_multiplier已經結束,factor仍被保留下來,形成閉包。
4. 函式可以儲存在資料結構中
因為函式是物件,我們可以把它放進 列表、字典、集合 等容器,進而實現「策略模式」或「指令模式」。
def add(a: int, b: int) -> int: return a + b
def sub(a: int, b: int) -> int: return a - b
def mul(a: int, b: int) -> int: return a * b
operations = {
"加法": add,
"減法": sub,
"乘法": mul,
}
def calculate(op_name: str, x: int, y: int) -> int:
"""根據 op_name 從字典中取出對應的函式並執行"""
func = operations.get(op_name)
if not func:
raise ValueError(f"不支援的運算:{op_name}")
return func(x, y)
print(calculate("加法", 7, 5)) # => 12
print(calculate("乘法", 7, 5)) # => 35
5. Lambda(匿名函式)與即時函式
lambda 讓我們 在一行內定義簡短的函式,常與高階函式一起使用。雖然功能上等同於 def,但可讀性與除錯性較差,應適度使用。
# 使用 lambda 計算平方
square = lambda x: x * x
print(square(6)) # => 36
# 在 sorted 中傳入自訂排序鍵
words = ["apple", "banana", "cherry", "date"]
words_sorted = sorted(words, key=lambda w: len(w))
print(words_sorted) # => ['date', 'apple', 'banana', 'cherry']
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的做法 |
|---|---|---|
| 忘記加括號 | 把函式名稱寫成 func() 會直接執行,失去「傳遞函式」的意圖。 |
傳遞時僅使用函式名稱(如 apply(func)),不加 ()。 |
| 閉包捕獲變數的變化 | 在迴圈裡建立閉包時,所有閉包會捕獲同一個變數,導致結果相同。 | 使用 預設參數 或 functools.partial 固定當前值。 |
| 過度使用 lambda | 內嵌過長的 lambda 會降低可讀性。 | 當邏輯超過一兩行時,改用 def 定義具名函式。 |
| 忽略型別提示 | 高階函式接受的 Callable 若未標註型別,易產生錯誤。 |
使用 typing.Callable、Protocol 為函式參數、返回值加上型別。 |
| 遺忘返回值 | 在返回函式的工廠函式中忘記 return,導致返回 None。 |
檢查每個路徑都有返回值,或使用 mypy 靜態檢查。 |
最佳實踐
- 保持函式純粹(pure):若函式不依賴外部狀態,則更易於作為回呼或傳遞。
- 使用
functools.wraps:在寫裝飾器時保留原函式的__name__、__doc__。 - 利用
partial:當需要「預先綁定」部分參數時,functools.partial是安全又可讀的選擇。 - 文件化回呼契約:在函式說明中明確列出回呼函式的參數與回傳型別,避免使用者踩雷。
- 測試高階函式:寫單元測試時,使用 假函式(mock) 來驗證回呼是否正確被呼叫。
from functools import partial
def power(base: int, exponent: int) -> int:
return base ** exponent
# 產生一個「平方」函式
square = partial(power, exponent=2)
print(square(5)) # => 25
實際應用場景
| 場景 | 為什麼需要一級公民函式 | 範例 |
|---|---|---|
| 事件驅動 GUI | UI 元件的點擊、變更等需要回呼函式。 | Tkinter 的 button.bind("<Button-1>", on_click) |
| 資料處理管線 | 把資料轉換步驟封裝成函式,串接成 pipeline。 | map, filter, reduce 搭配自訂函式 |
| Web 框架的中介層(middleware) | 中介層本身是接受 request、返回 response 的函式。 |
Flask 的 @app.before_request 裝飾器 |
| 策略模式 | 根據不同條件選擇不同演算法,使用字典映射。 | 前文的 operations 範例 |
| 裝飾器(Decorator) | 裝飾器本身是返回新函式的高階函式,用於功能擴充。 | @login_required、@cache 等 |
實務貼士:當你在設計 API 或函式庫時,盡量讓使用者能 自行注入回呼,這樣程式的彈性與可測試性都會大幅提升。
總結
- 函式是一等公民:它可以被指派、傳遞、儲存、返回,甚至在執行時動態產生。
- 了解 高階函式、閉包、lambda 與 資料結構內的函式,是寫出 可組合、可重用 程式碼的關鍵。
- 在開發過程中,避免常見陷阱(忘記加括號、閉包捕獲變數等),並遵循 型別提示、文件化契約、純粹函式 等最佳實踐。
- 真實專案中,從 GUI 事件、資料管線、Web 中介層到策略模式,皆可透過一級公民函式提升程式的彈性與可維護性。
掌握了「函式作為一級公民」的概念,你就能在 Python 的世界裡自由組合功能、打造乾淨的程式結構,為未來學習裝飾器、生成器、協程等更高階主題奠定堅實基礎。祝你寫程式愉快,持續探索 Python 的無限可能!