Python 課程 – 函式式編程 (Functional Programming)
主題:functools.partial 的使用與實務應用
簡介
在 Python 中,函式是一等公民,除了可以當作變數傳遞,還能透過高階函式 (higher‑order function) 產生新的行為。functools.partial 正是這類工具的典型代表,它允許我們「凍結」(freeze)函式的部分參數,產生一個更簡潔的可呼叫物件。
對於需要重複呼叫同一個函式但參數只有少數幾個會變動的情境,使用 partial 不僅可以減少程式碼重複,還能提升可讀性與可維護性。
在函式式編程的思維裡,我們常把「資料」與「行為」分離,透過組合 (composition) 與柯里化 (currying) 產生新函式。partial 正是 Python 中實作 柯里化 的最直接方式,也是許多第三方函式庫(如 requests、itertools)內部常用的技巧。
核心概念
1. functools.partial 的基本語法
from functools import partial
def greet(name, greeting='哈囉', punctuation='!'):
return f"{greeting} {name}{punctuation}"
# 只凍結 greeting 與 punctuation,產生一個只需要 name 的新函式
hello = partial(greet, greeting='哈囉', punctuation='!')
print(hello('小明')) # => 哈囉 小明!
partial(func, /, *args, **keywords)func:要被凍結的原始函式。*args:位置參數的預設值,會放在呼叫時的最前方。**keywords:關鍵字參數的預設值。
產生的 hello 仍然是一個 可呼叫(callable)物件,支援 __name__、__doc__ 等屬性(雖然名稱會變成 partial,可自行設定 __name__ 以提升除錯資訊)。
2. 與 lambda 的比較
許多新手會直接寫 lambda 來達成同樣的效果:
add_five = lambda x: add(x, 5)
雖然可行,但 partial 更具可讀性,且在處理多個參數或預設關鍵字時,lambda 會變得相當冗長。
# 使用 partial
add_five = partial(add, y=5) # y 為關鍵字參數
3. 結合其他高階函式
partial 常與 map、filter、sorted 等高階函式一起使用,讓程式碼更具宣告性。
from functools import partial
# 取得字串的前 n 個字元
def prefix(s, n):
return s[:n]
first_three = partial(prefix, n=3)
words = ['apple', 'banana', 'cherry']
result = list(map(first_three, words)) # ['app', 'ban', 'che']
4. 多層 partial(柯里化)
partial 本身也是可被再次凍結的,這讓我們能一步步構建函式。
def power(base, exponent):
return base ** exponent
# 先凍結 exponent 為 2,得到平方函式
square = partial(power, exponent=2)
# 再凍結 base 為 3,得到 3 的平方
cube_of_three = partial(square, base=3) # 等同於 power(3, 2)
print(cube_of_three()) # 9
5. partial 與類別方法
partial 也能用在類別的實例方法上,特別是當你想把某些參數預先寫死,然後交給回呼 (callback) 使用。
class Logger:
def __init__(self, prefix):
self.prefix = prefix
def log(self, level, message):
print(f"{self.prefix} [{level}] {message}")
logger = Logger("[MyApp]")
error_logger = partial(logger.log, level='ERROR')
error_logger("無法連線到資料庫") # => [MyApp] [ERROR] 無法連線到資料庫
程式碼範例
以下提供 5 個實用範例,每個範例都附上說明與常見的使用情境。
範例 1:簡化 requests 的 GET 呼叫
import requests
from functools import partial
# 預設的 headers 與 timeout
default_get = partial(requests.get,
headers={'User-Agent': 'MyCrawler/1.0'},
timeout=5)
resp = default_get('https://httpbin.org/get')
print(resp.status_code)
說明:把常用的
headers與timeout先凍結,之後只需要傳入 URL 即可。
範例 2:自訂排序鍵(key function)
data = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Carol', 'age': 27},
]
# 只取出 age 欄位作為排序鍵
by_age = partial(lambda d, key: d[key], key='age')
sorted_data = sorted(data, key=by_age)
print(sorted_data)
# => [{'name': 'Bob', 'age': 25}, {'name': 'Carol', 'age': 27}, {'name': 'Alice', 'age': 30}]
說明:利用
partial把key='age'事先寫死,讓sorted的key參數更直觀。
範例 3:建立「預設參數」的 CLI 命令
import argparse
from functools import partial
def run(task, verbose=False, dry_run=False):
"""執行指定任務,支援詳細模式與模擬執行。"""
if dry_run:
print(f"[dry-run] {task}")
else:
print(f"Running {task} {'(verbose)' if verbose else ''}")
parser = argparse.ArgumentParser()
parser.add_argument('task')
parser.add_argument('--verbose', action='store_true')
parser.add_argument('--dry', action='store_true')
args = parser.parse_args()
# 依照指令列參數產生不同的 partial
runner = partial(run, task=args.task, verbose=args.verbose, dry_run=args.dry)
runner()
說明:把 CLI 解析後的結果直接傳給
partial,可避免在if-else中重複寫參數。
範例 4:在 GUI 按鈕上綁定帶參數的回呼
import tkinter as tk
from functools import partial
def greet(name):
print(f"哈囉,{name}!")
root = tk.Tk()
root.title("Partial Demo")
# 為每個按鈕凍結不同的 name
for person in ['小明', '小華', '小美']:
btn = tk.Button(root, text=person,
command=partial(greet, name=person))
btn.pack(padx=10, pady=5)
root.mainloop()
說明:在 GUI 開發中,
partial能避免使用lambda捕獲迴圈變數的常見錯誤。
範例 5:多層 partial 實作簡易柯里化
def multiply(a, b, c):
return a * b * c
# 第一步:凍結 a = 2
mul_by_2 = partial(multiply, a=2)
# 第二步:凍結 b = 5,得到只需要 c 的函式
mul_by_2_and_5 = partial(mul_by_2, b=5)
print(mul_by_2_and_5(3)) # 2 * 5 * 3 = 30
說明:透過逐層凍結,我們可以把一個三元函式變成只接受單一參數的簡易「偏函式」。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方案或最佳實踐 |
|---|---|---|
| 忘記傳入必要的參數 | TypeError: missing required positional argument |
使用 IDE 或 inspect.signature 檢查原函式的參數列表。 |
| 凍結可變物件(如 list、dict) | 後續修改會影響所有 partial 產生的函式(共享同一個物件) |
若需要獨立副本,使用 copy.deepcopy 或在 partial 前先建立新物件。 |
partial 產生的函式失去原本的 __name__、__doc__ |
除錯或自動文件化時資訊不完整 | 手動設定 new_func.__name__ = original.__name__、new_func.__doc__ = original.__doc__,或使用 functools.update_wrapper. |
在多執行緒環境下共享同一個 partial 物件 |
競爭條件 (race condition) 若凍結的是可變物件 | 盡量把 partial 放在每個執行緒的本地變數中,或使用不可變的參數。 |
過度使用 partial 使程式碼抽象化過頭 |
可讀性下降,後續維護困難 | 只在重複呼叫、參數固定且意圖明確的情境使用,否則直接寫普通函式或 lambda。 |
最佳實踐:
- 保持簡潔:只凍結真正需要預設的參數,避免一次凍結過多。
- 使用說明文字:在
partial前加上註解,說明為何要凍結這些參數。 - 結合
functools.update_wrapper:保留原函式的元資訊,提升除錯效率。
from functools import partial, update_wrapper
def add(a, b):
"""回傳 a + b 的結果。"""
return a + b
add_five = partial(add, b=5)
update_wrapper(add_five, add) # 保留 __name__、__doc__
實際應用場景
API 客戶端封裝
- 把 API 金鑰、預設的 Header、timeout 等資訊凍結,讓每次呼叫只需提供 endpoint 或 payload。
資料處理管線 (pipeline)
- 在
map、filter、reduce之間傳遞已凍結參數的函式,讓管線更具可組合性。
- 在
測試與 Mock
- 使用
partial把測試函式的依賴參數固定,方便在unittest或pytest中重複使用。
- 使用
GUI 事件處理
- 為多個按鈕或選單項目快速綁定帶參數的回呼,避免
lambda捕獲迴圈變數的常見錯誤。
- 為多個按鈕或選單項目快速綁定帶參數的回呼,避免
命令列工具
- 把子指令的共用參數(如
verbose、dry-run)凍結,讓每個子指令的實作更乾淨。
- 把子指令的共用參數(如
總結
functools.partial 是 Python 函式式編程中不可或缺的工具,它讓我們能夠以「凍結參數」的方式快速產生新函式,從而提升程式碼的可讀性、可維護性與重用性。透過本篇文章,我們了解了:
partial的基本語法與與lambda的差異。- 如何在高階函式、類別方法、GUI 以及 CLI 中運用
partial。 - 常見的陷阱(如可變物件共享)與相應的最佳實踐。
- 真實世界的應用場景,從 API 客戶端到資料處理管線皆可受惠。
在日常開發中,適度使用 partial 能讓程式碼更加宣告式 (declarative),減少重複與錯誤。建議在寫完函式後,先思考是否有「固定參數」的情境,若有,就把它抽象為 partial,讓程式碼變得更簡潔、更易於維護。祝你在 Python 的函式式編程之路上玩得開心!