本文 AI 產出,尚未審核

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,回傳一個包住 funcwrapper
  • 使用 @timer 語法糖時,slow_sum 會被 timer 替換成 wrapper,因此每次呼叫 slow_sum 都會自動執行計時邏輯。

常見陷阱與最佳實踐

陷阱 說明 建議的解決方式
可變預設參數 把可變物件(如 listdict)作為函式預設值,會在多次呼叫間共享同一個實例。 使用 None 作為預設值,並在函式內部自行建立新物件。
過度使用匿名函式 (lambda) lambda 只能寫單行表達式,過度濫用會讓程式碼難以閱讀。 只在簡單、一次性的小轉換時使用;複雜邏輯請寫成具名函式。
忘記 functools.wraps 自訂裝飾器若未使用 wraps,會遺失原函式的 __name____doc__ 等資訊,影響除錯與文件生成。 在裝飾器內部使用 @wraps(func)
過度嵌套高階函式 連續使用 mapfilterreduce 會產生「函式巢狀」的寫法,閱讀成本提升。 考慮使用列表生成式或 itertools 的管線化(pipeline)方式,保持可讀性。
效能瓶頸 高階函式本身不會自動提升效能,特別是對大資料集使用 map/filter 時若不配合惰性求值(如 itertools),會一次性產生大量中間結果。 使用 itertools(如 imapifilter)或生成器表達式,讓資料流保持惰性。

最佳實踐小結

  1. 保持函式純粹:盡量讓高階函式的參數與回傳值不產生副作用,這樣組合時較不會產生意外行為。
  2. 適度使用型別註解:在函式簽名中加入 Callable[[T], R] 等型別,可提高 IDE 的自動完成與靜態檢查能力。
  3. 善用內建函式:Python 已提供 mapfiltersortedanyall 等高階函式,除非有特殊需求,盡量不要自行重造。
  4. 文件化與測試:對每個自訂的高階函式寫清楚的 docstring,並配合單元測試(pytest)驗證其行為。

實際應用場景

1. 資料清洗與轉換管線(ETL)

在資料分析或機器學習前,常需要 抽取 → 轉換 → 載入(ETL)流程。利用 mapfilterreduce 或自訂的高階函式,我們可以把每一步抽象為獨立的函式,然後以管線方式串接:

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. 自訂排序與分組

sortedgroupbyitertools)皆接受「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 內建語法的強大延伸。透過 mapfilterreducepartial、自訂裝飾器等,我們能夠:

  • 抽象出重複的遍歷與條件邏輯
  • 純函式 + 不可變資料 的方式提升程式的可測試性與可預測性
  • 建立彈性且可組合的資料處理管線,或是簡化事件回呼與控制結構

在實務開發中,建議先從 內建高階函式 入手,熟悉它們的行為與限制,再逐步設計自己的高階函式或裝飾器。記得遵循「保持純粹、避免副作用、加上完整文件與測試」的最佳實踐,才能讓程式碼既 簡潔可靠

祝你在 Python 的函式式編程之路上玩得開心,寫出更具表達力與彈性的程式! 🚀