本文 AI 產出,尚未審核

Python – 函式式編程:Immutability 與 Pure Function 概念


簡介

在 Python 這門多範式語言中,函式式編程 (Functional Programming) 提供了一套避免副作用、提升程式可預測性的思考方式。兩個最核心的概念——不可變性 (immutability)純函式 (pure function),不僅能讓程式更易於測試與除錯,還能在多執行緒、分散式系統等高併發情境下提升安全性。

對於剛踏入 Python 的新手或已具備基礎的開發者而言,了解這兩個概念並能在實務中正確運用,是邁向更乾淨、可維護程式碼的重要一步。本篇文章將以 簡單易懂的語言實用範例,說明什麼是不可變物件與純函式、它們的好處、常見陷阱,以及在真實專案中如何落地。


核心概念

1️⃣ 什麼是不可變性 (Immutability)

在程式語言裡,資料結構大致可分為 可變 (mutable)不可變 (immutable) 兩類。

  • 可變物件:其內部狀態可以在建立後被改變,例如 listdictset
  • 不可變物件:一旦建立,其內容就不能被更改,例如 intfloatstrtuplefrozenset

為什麼要偏好不可變物件?

  • 不可變物件天然 thread‑safe,多執行緒同時讀取不會產生競爭條件。
  • 在函式式編程中,資料的「傳遞」不會產生副作用,讓程式的行為更可預測。

範例 1:使用 tuple 替代 list

# 可變的 list
numbers = [1, 2, 3]
numbers.append(4)          # 改變了原本的 list
print(numbers)             # [1, 2, 3, 4]

# 不可變的 tuple
numbers_imm = (1, 2, 3)
# numbers_imm.append(4)    # AttributeError: 'tuple' object has no attribute 'append'
new_numbers = numbers_imm + (4,)   # 產生新 tuple,原本的 tuple 不變
print(new_numbers)         # (1, 2, 3, 4)

重點tuple 本身不會被改變,若需要「新增」元素,會回傳全新物件,而不是改寫原本的資料。

2️⃣ 什麼是純函式 (Pure Function)

純函式 必須同時滿足兩個條件:

  1. 相同的輸入永遠產生相同的輸出(沒有隨機性、時間或全域狀態的影響)。
  2. 不會產生副作用(不會修改參數、全域變數、檔案、資料庫等外部資源)。

純函式的好處包括:

  • 易於測試:只要驗證輸入與輸出即可。
  • 可組合:純函式之間可以自由組合,不必擔心相互干擾。
  • 提升可併發性:因為不會改變共享狀態,天然適合平行執行。

範例 2:純函式 vs. 非純函式

# 非純函式:依賴全域變數,且修改了傳入的 list
total = 0
def accumulate(values):
    global total
    for v in values:
        total += v          # 改變全域狀態
    return total

print(accumulate([1, 2, 3]))   # 6
print(accumulate([1, 2]))      # 9  ← 前一次的結果被保留下來,行為不一致

# 純函式:僅依賴輸入,回傳新值,不改變任何外部狀態
def pure_sum(values):
    return sum(values)          # 內建 sum 本身是純函式

print(pure_sum([1, 2, 3]))     # 6
print(pure_sum([1, 2]))        # 3  ← 每次呼叫都與輸入直接對應

3️⃣ 結合不可變性與純函式的寫法

在 Python 中,我們可以藉由 資料結構的不可變化,配合 純函式,打造「函式式」的程式碼風格。

範例 3:使用 namedtuplemapfilter

from collections import namedtuple

# 定義不可變的資料結構
Point = namedtuple('Point', ['x', 'y'])

points = (Point(1, 2), Point(3, 4), Point(-1, -5))

# 純函式:計算每個點到原點的距離
def distance(p: Point) -> float:
    return (p.x ** 2 + p.y ** 2) ** 0.5

# 使用 map 產生新序列,原始 points 不會被改變
distances = tuple(map(distance, points))
print(distances)   # (2.23606797749979, 5.0, 5.0990195135927845)

範例 4:利用 functools.reduce 實作純粹的累計

from functools import reduce

# 純函式:二元運算子
def add(a, b):
    return a + b

numbers = (10, 20, 30)

# reduce 會把序列折疊成單一值,整個過程不會改變原始 numbers
total = reduce(add, numbers, 0)   # 初始值 0
print(total)  # 60

範例 5:使用 dataclassesfrozen=True 建立不可變物件

from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    id: int
    name: str

alice = User(1, "Alice")
# alice.name = "Bob"   # dataclasses.FrozenInstanceError: cannot assign to field 'name'

# 純函式:產生新 User,保留原本的 id
def rename(user: User, new_name: str) -> User:
    return User(user.id, new_name)

bob = rename(alice, "Bob")
print(alice)  # User(id=1, name='Alice')
print(bob)    # User(id=1, name='Bob')

小結:以上範例展示了「不可變資料 + 純函式」的典型寫法;每一次的「變更」其實是產生新物件,而不是直接改寫舊物件。


常見陷阱與最佳實踐

陷阱 說明 解決方式
誤以為 list/dict 是不可變的 直接把可變容器傳給純函式,函式內部若不小心修改,會產生副作用。 使用 tuplefrozenset,或在函式內 先 copy (list(obj)obj.copy()) 再操作。
全域變數的隱藏依賴 純函式若不小心讀取或寫入全域變數,會破壞可預測性。 把所有需要的資訊作為參數傳入,或使用 dependency injection
Mutable default arguments def foo(arg=[]): 會共用同一個列表,導致意外狀態累積。 使用 None 作為預設值,並在函式內部建立新容器:if arg is None: arg = []
忘記返回新物件 有時候寫了「改變」的程式碼卻忘了 return,結果是 None,呼叫端無法取得新值。 明確寫 return new_obj,或使用 pipelinepipe)風格。
過度使用 deepcopy 為了避免副作用而大量 deep copy,會帶來效能問題。 盡量選擇天生不可變的結構,或只在必要的層級 copy。

最佳實踐

  1. 盡量使用不可變容器tuplefrozensetnamedtupledataclass(frozen=True)
  2. 讓函式保持純粹:只接受參數、只回傳值,避免 I/O、全域狀態。
  3. 函式組合:利用 mapfilterreduceitertools 等工具鏈接純函式,提升可讀性。
  4. 單元測試:純函式的測試只需要檢查輸入/輸出,寫測試時可省去 mock 之類的複雜設定。
  5. 文件說明:在函式 docstring 中標註「pure」或「immutable」以提醒使用者其行為特性。

實際應用場景

場景 為何適合使用不可變 + 純函式 示例
資料分析的管線 (ETL) 每一步驟產生新資料表,避免前一步的變更影響後續步驟。 df_clean = df.dropna().assign(new_col=lambda x: x['a']*2)(Pandas 雖非純函式,但可透過 copy() 保持不可變)
多執行緒或協程 不可變資料不需要鎖 (lock),降低死結風險。 concurrent.futures.ThreadPoolExecutor 中傳遞 tuplefrozenset 給 worker。
函式式 API 設計 讓 API 呼叫者只關心輸入與輸出,方便組合與重用。 def paginate(items: tuple, page: int, size: int) -> tuple:
遊戲或模擬系統的狀態管理 使用 immutable state tree(類似 Redux)追蹤每一次狀態變更,方便 undo/redo。 state = State(board=tuple(board), turn='X')new_state = state.move(pos)
金融或科學計算 結果必須可重現,純函式保證相同輸入得到相同結果。 def price_option(s, k, t, r, sigma):(純函式)

總結

  • 不可變性 讓資料本身成為「只讀」的資源,天然支援多執行緒與函式式組合。
  • 純函式 則保證「相同輸入 → 相同輸出」且不產生副作用,讓程式更易於測試、除錯與重構。
  • 在 Python 中,我們可以透過 tuplefrozensetnamedtupledataclass(frozen=True) 等工具,輕鬆建立不可變物件;再配合 mapfilterreduceitertools 等高階函式,寫出 純粹且可組合 的程式碼。
  • 謹記常見陷阱(可變容器、全域依賴、mutable default arguments),並遵循最佳實踐(使用不可變容器、保持函式純粹、寫清楚的 docstring),即可在日常開發或大型系統中,享受到函式式編程帶來的 可預測性、可維護性與高併發安全

挑戰自己:在下個小專案中,嘗試把資料流改寫為「不可變 → 純函式」的方式,你會發現程式碼的可讀性與測試成本都有明顯提升。祝你玩得開心,寫出更乾淨的 Python 程式!