本文 AI 產出,尚未審核

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. 關於 nonlocalglobal

  • 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 釋放引用。
可讀性下降 過度使用閉包會讓程式流程變得不易追蹤。 只在需要保持狀態或抽象化時使用,平時可考慮類別或簡單函式。

最佳實踐

  1. 保持簡潔:閉包的內部函式最好只做一件事,避免過度複雜。
  2. 使用 functools.wraps:在寫裝飾器時保留原函式的 __name____doc__ 等資訊。
  3. 凍結迴圈變數
    funcs = []
    for i in range(5):
        funcs.append(lambda i=i: i)   # i=i 讓 i 的當前值成為預設參數
    
  4. 測試閉包行為:利用 __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 函式的一種特殊形態,允許函式捕獲並保存外層作用域的變數。
  • 透過 nonlocallambda、以及裝飾器等技巧,我們可以在不使用全域變數或類別的情況下,實現 保持狀態延遲執行抽象化 等功能。
  • 使用閉包時要注意迴圈變數捕獲、記憶體占用以及程式可讀性,遵守 最佳實踐 能讓程式更安全、易維護。
  • 事件回呼緩存API 客戶端 等實務場景中,閉包都是提升程式彈性與效率的有力工具。

掌握閉包的概念與技巧,將為你的 Python 程式設計之路增添更深層的思考方式,也讓你在日後開發大型系統或撰寫高階函式庫時,能夠更從容不迫。祝你玩得開心,寫得更好!