Python 函式(Functions)— 閉包(Closure)
簡介
在程式設計中,函式是最基本的抽象單位,而 閉包 則是函式的進階概念之一。
閉包允許我們在函式外部「保存」一段環境(變數)資訊,讓這段資訊能在函式被呼叫的任何時候仍然可用。
掌握閉包不僅能寫出更簡潔、可重用的程式碼,還能在 裝飾器(decorator)、函式工廠(function factory)、延遲執行(lazy evaluation) 等實務情境中發揮關鍵作用。對於剛踏入 Python 的新手與已有一定基礎的開發者來說,了解閉包的機制與使用方式,是提升程式設計思維的重要一步。
核心概念
1. 什麼是閉包?
簡單來說,閉包 是由 內部函式(nested function)和 其外層環境(enclosing scope)所組成的物件。
當一個內部函式參考了外層函式的變數時,即使外層函式已經結束執行,這些變數仍會被「捕獲」在內部函式裡,形成閉包。
關鍵點:
- 內部函式必須 引用(reference)外層變數。
- Python 會自動將這些被引用的變數封裝在
__closure__屬性中。
2. 為何需要閉包?
- 保持狀態:在不使用全域變數或類別的情況下,讓函式保有先前的計算結果。
- 延遲執行:把計算的時機推遲到真正需要結果的時候。
- 抽象化:將重複的設定或行為抽離成可重用的函式工廠。
3. 基本範例:計數器
def make_counter():
"""回傳一個可以遞增的計數器函式(閉包)"""
count = 0 # 外層變數
def counter():
nonlocal count # 宣告要修改外層變數
count += 1
return count
return counter # 回傳內部函式,形成閉包
c1 = make_counter()
print(c1()) # 1
print(c1()) # 2
c2 = make_counter()
print(c2()) # 1 ← 與 c1 完全獨立
說明:
counter捕獲了make_counter中的count變數,且每次呼叫c1()時都會操作同一個count,形成持續的狀態。
4. 閉包與 lambda
lambda 也是建立閉包的常見方式,尤其在需要簡短函式時非常便利。
def power_factory(exp):
"""回傳一個計算 x 的 exp 次方的函式(閉包)"""
return lambda x: x ** exp
square = power_factory(2)
cube = power_factory(3)
print(square(5)) # 25
print(cube(2)) # 8
這裡 lambda x: x ** exp 捕獲了 exp,即使 power_factory 已經結束,exp 仍然存在於返回的函式內。
5. 裝飾器(Decorator)— 閉包的典型應用
裝飾器本質上就是接受一個函式,回傳另一個「包裝過」的函式。這個包裝函式往往利用閉包保存原始函式的參考。
import time
from functools import wraps
def timer(func):
"""計算被裝飾函式執行時間的裝飾器"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # func 為閉包捕獲的原始函式
end = time.time()
print(f"{func.__name__} 執行時間: {end - start:.4f} 秒")
return result
return wrapper
@timer
def heavy_compute(n):
total = 0
for i in range(n):
total += i ** 2
return total
print(heavy_compute(10_0000))
wrapper 捕獲了 func(即被裝飾的 heavy_compute),形成閉包,使得每次呼叫 heavy_compute 時,都會先走過計時的前置與後置程式碼。
6. 關於 nonlocal 與 global
nonlocal用於 修改最近一層(但非全域)作用域的變數。global用於 修改全域作用域的變數。
在閉包中,若要改變外層變數的值,必須使用 nonlocal,否則 Python 會把賦值視為建立新變數,導致錯誤或不符合預期。
def accumulator():
total = 0
def add(x):
nonlocal total
total += x
return total
return add
acc = accumulator()
print(acc(5)) # 5
print(acc(3)) # 8
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 變數被覆蓋 | 在迴圈內建立閉包時,所有閉包會捕獲同一個迴圈變數,導致結果相同。 | 使用 預設參數 或 functools.partial 來「凍結」當前值。 |
忘記 nonlocal |
內部函式想修改外層變數卻未宣告 nonlocal,會拋 UnboundLocalError。 |
明確加上 nonlocal,或改用可變容器(如 list、dict)繞過。 |
| 記憶體泄漏 | 閉包持有大量外層資料,可能導致不必要的記憶體佔用。 | 僅捕獲必要變數,或在不再需要時手動 del 釋放引用。 |
| 可讀性下降 | 過度使用閉包會讓程式流程變得不易追蹤。 | 只在需要保持狀態或抽象化時使用,平時可考慮類別或簡單函式。 |
最佳實踐
- 保持簡潔:閉包的內部函式最好只做一件事,避免過度複雜。
- 使用
functools.wraps:在寫裝飾器時保留原函式的__name__、__doc__等資訊。 - 凍結迴圈變數:
funcs = [] for i in range(5): funcs.append(lambda i=i: i) # i=i 讓 i 的當前值成為預設參數 - 測試閉包行為:利用
__closure__屬性檢查捕獲的變數,確保預期的閉包結構。
實際應用場景
1. 事件驅動的回呼(Callback)
在 GUI 或非同步程式中,常需要把某些參數「預先填好」再交給事件系統。閉包可讓我們在註冊回呼時,同時保存當前的狀態。
def on_button_click(msg):
def handler(event):
print(f"Button clicked! Message: {msg}")
return handler
# 假設 btn 是某個 UI 元件
btn.bind("<Button-1>", on_button_click("你好,世界"))
2. 計算緩存(Memoization)
閉包可以保存已計算的結果,避免重複運算。
def memoize(fn):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
result = fn(*args)
cache[args] = result
return result
return wrapper
@memoize
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(35)) # 第一次較慢,之後快很多
3. 動態產生 API 客戶端
根據不同的 API 端點與授權資訊,閉包可以快速產生對應的呼叫函式。
import requests
def api_client(base_url, token):
def request(endpoint, **params):
headers = {"Authorization": f"Bearer {token}"}
return requests.get(f"{base_url}/{endpoint}", headers=headers, params=params).json()
return request
github = api_client("https://api.github.com", "YOUR_TOKEN")
print(github("users/octocat"))
總結
- 閉包 是 Python 函式的一種特殊形態,允許函式捕獲並保存外層作用域的變數。
- 透過
nonlocal、lambda、以及裝飾器等技巧,我們可以在不使用全域變數或類別的情況下,實現 保持狀態、延遲執行、抽象化 等功能。 - 使用閉包時要注意迴圈變數捕獲、記憶體占用以及程式可讀性,遵守 最佳實踐 能讓程式更安全、易維護。
- 在 事件回呼、緩存、API 客戶端 等實務場景中,閉包都是提升程式彈性與效率的有力工具。
掌握閉包的概念與技巧,將為你的 Python 程式設計之路增添更深層的思考方式,也讓你在日後開發大型系統或撰寫高階函式庫時,能夠更從容不迫。祝你玩得開心,寫得更好!