Python 課程 – 函式式編程 (Functional Programming)
主題:高階函式(Higher‑Order Function)
簡介
在 Python 中,函式是一等公民(first‑class citizen),也就是說函式本身可以像變數一樣被傳遞、賦值或作為參數傳入其他函式。正因如此,我們可以寫出 高階函式——接受函式作為參數,或回傳函式本身的函式。
高階函式是函式式編程的核心概念之一,它能讓程式碼更具抽象化、可重用性與可組合性。透過高階函式,我們可以輕鬆構建資料處理管線(pipeline)、實作事件回呼(callback)或建立自訂的控制結構,從而寫出 更簡潔、可讀且易於測試 的程式。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐一帶領讀者掌握在 Python 中使用高階函式的技巧,並提供實務上常見的應用情境,幫助你在日常開發中活用這個強大的工具。
核心概念
1. 什麼是高階函式?
高階函式(higher‑order function)指的是接受函式作為參數,或回傳另一個函式的函式。
在 Python 中,常見的內建高階函式包括map()、filter()、sorted()、reduce()(在functools模組),以及自訂的裝飾器(decorator)等。
2. 為什麼要使用高階函式?
- 抽象化重複邏輯:把「遍歷」或「條件判斷」等通用模式抽出成函式,讓程式碼只關心「要做什麼」而非「怎麼做」。
- 提升可組合性:高階函式往往回傳新函式,這讓我們可以將多個小功能「串接」成更大的流程。
- 支援函式式思考:配合 純函式(pure function)與 不可變資料(immutable data),能寫出更易於推理與測試的程式。
程式碼範例
以下示範 5 個實用的高階函式範例,從內建函式到自訂裝飾器,皆附有說明與註解。
2.1 map() – 把函式套用到可疊代物件的每個元素
# 把攝氏溫度轉換成華氏溫度
def c_to_f(celsius: float) -> float:
"""將攝氏轉換為華氏"""
return celsius * 9 / 5 + 32
celsius_list = [0, 20, 37, 100]
# map 接受兩個參數:函式與可疊代物件,回傳一個 map 物件(可轉成 list)
fahrenheit = list(map(c_to_f, celsius_list))
print(fahrenheit) # [32.0, 68.0, 98.60000000000001, 212.0]
map本身即是一個 高階函式,它把c_to_f這個函式「映射」到celsius_list的每個元素上。
2.2 filter() – 篩選符合條件的元素
# 篩選出質數(簡易版)
def is_prime(n: int) -> bool:
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
numbers = range(1, 21)
# filter 會把 is_prime 作為條件函式,僅保留回傳 True 的元素
primes = list(filter(is_prime, numbers))
print(primes) # [2, 3, 5, 7, 11, 13, 17, 19]
2.3 functools.reduce() – 把序列「縮減」成單一值
from functools import reduce
from operator import mul
# 計算一串數字的階乘(使用 reduce)
def factorial(n: int) -> int:
"""回傳 n 的階乘"""
if n < 0:
raise ValueError("n 必須是非負整數")
# range(1, n+1) 產生 1~n 的序列,mul 為乘法運算子
return reduce(mul, range(1, n + 1), 1)
print(factorial(6)) # 720
reduce 接收三個參數:二元函式(此例為 operator.mul)、可疊代物件、以及初始值(1)。它會把序列的每兩個元素依次套用函式,最終「縮減」成單一結果。
2.4 functools.partial() – 產生「預先綁定」參數的函式
from functools import partial
def power(base: int, exponent: int) -> int:
"""計算 base 的 exponent 次方"""
return base ** exponent
# 建立一個只需要傳入 exponent 的新函式,base 預設為 2
square = partial(power, base=2)
cube = partial(power, base=2, exponent=3)
print(square(5)) # 2**5 = 32
print(cube()) # 2**3 = 8
partial 本身就是一個 高階函式,它把原始函式的部份參數「凍結」,回傳一個新的可呼叫物件,讓呼叫端更簡潔。
2.5 自訂裝飾器 – 回傳新函式的高階函式
import time
from functools import wraps
def timer(func):
"""計時裝飾器:測量 func 執行時間"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # 呼叫原始函式
end = time.perf_counter()
print(f"{func.__name__} 執行時間:{end - start:.6f} 秒")
return result
return wrapper # <-- 這裡回傳的是 wrapper(新函式)
@timer
def slow_sum(n: int) -> int:
total = 0
for i in range(n):
total += i
return total
print(slow_sum(10_000_000))
timer接收一個函式func,回傳一個包住func的wrapper。- 使用
@timer語法糖時,slow_sum會被timer替換成wrapper,因此每次呼叫slow_sum都會自動執行計時邏輯。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
| 可變預設參數 | 把可變物件(如 list、dict)作為函式預設值,會在多次呼叫間共享同一個實例。 |
使用 None 作為預設值,並在函式內部自行建立新物件。 |
過度使用匿名函式 (lambda) |
lambda 只能寫單行表達式,過度濫用會讓程式碼難以閱讀。 |
只在簡單、一次性的小轉換時使用;複雜邏輯請寫成具名函式。 |
忘記 functools.wraps |
自訂裝飾器若未使用 wraps,會遺失原函式的 __name__、__doc__ 等資訊,影響除錯與文件生成。 |
在裝飾器內部使用 @wraps(func)。 |
| 過度嵌套高階函式 | 連續使用 map、filter、reduce 會產生「函式巢狀」的寫法,閱讀成本提升。 |
考慮使用列表生成式或 itertools 的管線化(pipeline)方式,保持可讀性。 |
| 效能瓶頸 | 高階函式本身不會自動提升效能,特別是對大資料集使用 map/filter 時若不配合惰性求值(如 itertools),會一次性產生大量中間結果。 |
使用 itertools(如 imap、ifilter)或生成器表達式,讓資料流保持惰性。 |
最佳實踐小結
- 保持函式純粹:盡量讓高階函式的參數與回傳值不產生副作用,這樣組合時較不會產生意外行為。
- 適度使用型別註解:在函式簽名中加入
Callable[[T], R]等型別,可提高 IDE 的自動完成與靜態檢查能力。 - 善用內建函式:Python 已提供
map、filter、sorted、any、all等高階函式,除非有特殊需求,盡量不要自行重造。 - 文件化與測試:對每個自訂的高階函式寫清楚的 docstring,並配合單元測試(
pytest)驗證其行為。
實際應用場景
1. 資料清洗與轉換管線(ETL)
在資料分析或機器學習前,常需要 抽取 → 轉換 → 載入(ETL)流程。利用 map、filter、reduce 或自訂的高階函式,我們可以把每一步抽象為獨立的函式,然後以管線方式串接:
from itertools import islice
def read_lines(path: str):
with open(path, encoding='utf-8') as f:
for line in f:
yield line.strip()
def to_numbers(line: str):
return [int(x) for x in line.split(',') if x]
def filter_positive(nums):
return list(filter(lambda x: x > 0, nums))
def sum_all(nums):
return sum(nums)
# 組合管線
total = sum(
map(
sum_all,
map(
filter_positive,
map(to_numbers, read_lines('data.csv'))
)
)
)
print(f"正數總和:{total}")
2. 事件驅動的回呼機制
在 GUI、網路伺服器或非同步程式中,回呼函式 常以高階函式的形式傳入框架:
import asyncio
def retry(times: int):
"""回呼裝飾器:若失敗則重試指定次數"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return await func(*args, **kwargs)
except Exception as e:
if attempt == times - 1:
raise
print(f"第 {attempt+1} 次失敗,重試中…")
return wrapper
return decorator
@retry(times=3)
async def fetch(url: str):
# 假設這裡有異步 HTTP 請求
...
# 在事件迴圈中使用
asyncio.run(fetch('https://example.com'))
3. 自訂排序與分組
sorted、groupby(itertools)皆接受「key」函式作為參數,這正是高階函式的應用:
from itertools import groupby
students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 92},
{'name': 'Cindy', 'score': 78},
{'name': 'David', 'score': 92},
]
# 依成績降序排序
sorted_students = sorted(students, key=lambda s: s['score'], reverse=True)
# 依成績分組(先排序再 groupby)
sorted_by_score = sorted(students, key=lambda s: s['score'])
for score, group in groupby(sorted_by_score, key=lambda s: s['score']):
print(f"Score {score}: {[s['name'] for s in group]}")
總結
高階函式是 函式式編程 的基石,也是 Python 內建語法的強大延伸。透過 map、filter、reduce、partial、自訂裝飾器等,我們能夠:
- 抽象出重複的遍歷與條件邏輯
- 以 純函式 + 不可變資料 的方式提升程式的可測試性與可預測性
- 建立彈性且可組合的資料處理管線,或是簡化事件回呼與控制結構
在實務開發中,建議先從 內建高階函式 入手,熟悉它們的行為與限制,再逐步設計自己的高階函式或裝飾器。記得遵循「保持純粹、避免副作用、加上完整文件與測試」的最佳實踐,才能讓程式碼既 簡潔 又 可靠。
祝你在 Python 的函式式編程之路上玩得開心,寫出更具表達力與彈性的程式! 🚀