本文 AI 產出,尚未審核

Python 課程 – 函式式編程 (Functional Programming)

主題:functools.partial 的使用與實務應用


簡介

在 Python 中,函式是一等公民,除了可以當作變數傳遞,還能透過高階函式 (higher‑order function) 產生新的行為。functools.partial 正是這類工具的典型代表,它允許我們「凍結」(freeze)函式的部分參數,產生一個更簡潔的可呼叫物件
對於需要重複呼叫同一個函式但參數只有少數幾個會變動的情境,使用 partial 不僅可以減少程式碼重複,還能提升可讀性與可維護性。

在函式式編程的思維裡,我們常把「資料」與「行為」分離,透過組合 (composition) 與柯里化 (currying) 產生新函式。partial 正是 Python 中實作 柯里化 的最直接方式,也是許多第三方函式庫(如 requestsitertools)內部常用的技巧。


核心概念

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 常與 mapfiltersorted 等高階函式一起使用,讓程式碼更具宣告性。

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)

說明:把常用的 headerstimeout 先凍結,之後只需要傳入 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}]

說明:利用 partialkey='age' 事先寫死,讓 sortedkey 參數更直觀。

範例 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

最佳實踐

  1. 保持簡潔:只凍結真正需要預設的參數,避免一次凍結過多。
  2. 使用說明文字:在 partial 前加上註解,說明為何要凍結這些參數。
  3. 結合 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__

實際應用場景

  1. API 客戶端封裝

    • 把 API 金鑰、預設的 Header、timeout 等資訊凍結,讓每次呼叫只需提供 endpoint 或 payload。
  2. 資料處理管線 (pipeline)

    • mapfilterreduce 之間傳遞已凍結參數的函式,讓管線更具可組合性。
  3. 測試與 Mock

    • 使用 partial 把測試函式的依賴參數固定,方便在 unittestpytest 中重複使用。
  4. GUI 事件處理

    • 為多個按鈕或選單項目快速綁定帶參數的回呼,避免 lambda 捕獲迴圈變數的常見錯誤。
  5. 命令列工具

    • 把子指令的共用參數(如 verbosedry-run)凍結,讓每個子指令的實作更乾淨。

總結

functools.partial 是 Python 函式式編程中不可或缺的工具,它讓我們能夠以「凍結參數」的方式快速產生新函式,從而提升程式碼的可讀性、可維護性與重用性。透過本篇文章,我們了解了:

  • partial 的基本語法與與 lambda 的差異。
  • 如何在高階函式、類別方法、GUI 以及 CLI 中運用 partial
  • 常見的陷阱(如可變物件共享)與相應的最佳實踐。
  • 真實世界的應用場景,從 API 客戶端到資料處理管線皆可受惠。

在日常開發中,適度使用 partial 能讓程式碼更加宣告式 (declarative),減少重複與錯誤。建議在寫完函式後,先思考是否有「固定參數」的情境,若有,就把它抽象為 partial,讓程式碼變得更簡潔、更易於維護。祝你在 Python 的函式式編程之路上玩得開心!