深入理解 Python 中的閉包(Closure)

更新日期: 2024 年 11 月 18 日

本文為 python 作用域系列文,第一篇:

閉包是指內部函數,能夠記住其所在的外部函數中的變量,即使外部函數執行完畢、局部變量理應被銷毀時,這些變量依然能被內部函數訪問。

閉包的存在讓我們能實現一些巧妙的邏輯,常用於回調函數、裝飾器等場景。

接下來,我們將從代碼範例入手,逐步解析閉包的形成與運作原理。

閱讀本文前,建議具備相關概念:


基本閉包範例

以下是一個典型的 Python 閉包範例:

def hi():
    a = 1  # 外部函數的局部變量
    def hey():
        print(a)  # 訪問外部函數的局部變量
    return hey  # 將內部函數作為返回值

new_hey = hi()  # 執行 hi 函數,獲得內部函數 hey 的引用
new_hey()  # 調用內部函數 hey,輸出: 1

函數執行的流程

  1. hi() 執行時
    • hi() 的作用域內,變量 a 被賦值為 1
    • 定義了內部函數 hey,該函數訪問了外部函數的變量 a
    • 函數 hi 返回了內部函數 hey 的引用。
  1. 執行完 hi()
    • 按理說,hi() 的執行結束會銷毀其作用域內的局部變量 a
    • 但由於內部函數 hey 引用了變量 a,Python 建立了一個特殊的結構來保存 a 的值,這就是「細胞物件」(Cell Object)。
  1. 調用 new_hey()
    • 內部函數 hey 被執行,通過細胞物件引用到了變量 a 的值 1,從而輸出了結果。

細胞物件(Cell Object)的作用

細胞物件是閉包形成的關鍵。

當外部函數中的變量被內部函數引用時,Python 會自動建立細胞物件來保存這些變量的值,即使外部函數已結束,這些變量依然可以被內部函數訪問。

讓我們通過簡化的圖解,來展示細胞物件的關係:

閉包形成前

hi.a  ------------->  1
hi.hey.a  --------->  (直接指向 hi.a)

閉包形成後

hi() 執行完畢,局部變量 a 本應銷毀,但細胞物件將其保存了下來:

hi.a  ------------->  (消失)
hi.hey.a  --------->  [Cell Object] ----->  1

3. 驗證細胞物件的存在

Python 提供了檢查細胞物件的方法:

def hi():
    a = 1
    def hey():
        print(a)
    return hey

new_hey = hi()
print(new_hey.__closure__)  # 輸出: (<cell at 0x...: int object at 0x...>,)
  • __closure__ 屬性
    • __closure__ 是內部函數的一個屬性,返回所有被捕捉的變量。
    • 每個捕捉的變量都存放在一個細胞物件中。

延伸與應用

多變量閉包

閉包可以捕捉多個變量:

def outer(x):
    def inner(y):
        return x + y  # 捕捉外部變量 x
    return inner

add_five = outer(5)
print(add_five(10))  # 輸出: 15

此例中,變量 x 被內部函數 inner 捕捉並保存在細胞物件中,內部函數執行時即可訪問。


注意事項:變量可變性

細胞物件中保存的變量是共享的,因此修改外部變量時需謹慎:

def counter():
    count = 0
    def increment():
        nonlocal count  # 修改外部變量
        count += 1
        return count
    return increment

c = counter()
print(c())  # 輸出: 1
print(c())  # 輸出: 2

使用 nonlocal 關鍵字可在內部函數中修改外部變量,這在計數器、狀態追蹤等情境中非常實用。


總結

閉包是 Python 中非常強大的特性,它通過細胞物件實現了內部函數,對外部作用域變量的持久訪問。

理解閉包的運作方式,有助於開發者靈活處理,多層函數嵌套以及狀態保存的需求。

學會檢查 __closure__ 屬性並合理使用閉包,將使你的 Python 編碼技能更上一層樓!

Similar Posts