聊聊 Python 中的閉包

發表於2016-04-02
閉包(Closure)

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。
[維基百科::閉包(電腦科學)]

0x02 Python 中的函數語言程式設計 本來也應該包括閉包的概念,但是我覺得閉包更重要的是對作用域(Scope)的理解,因此把它單獨列出來,同時可以理順一下 Python 的作用域規則。

閉包的概念最早出現在函數語言程式設計語言中,後來被一些指令式程式設計語言所借鑑。尤其是在一些函式作為一等公民的語言中,例如JavaScript就經常用到(在JavaScript中函式幾乎可以當做“特等公民”看待),我之前也寫過一篇關於JavaScript閉包的文章(圖解Javascript上下文與作用域),實際上閉包並不是太複雜的概念,但是可以藉助閉包更好地理解不同語言的作用域規則。

名稱空間與作用域

0x00 The Zen of Python的最後一句重點強調名稱空間的概念,我們可以把名稱空間看做一個大型的字典型別(Dict),裡面包含了所有變數的名字和值的對映關係。在 Python 中,作用域實際上可以看做是“在當前上下文的位置,獲取名稱空間變數的規則”。在 Python 程式碼執行的任意位置,都至少存在三層巢狀的作用域:

  1. 最內層作用域,最早搜尋,包含所有區域性變數(Python 預設所有變數宣告均為區域性變數)
  2. 所有包含當前上下文的外層函式的作用域,由內而外依次搜尋,這裡包含的是非區域性非全域性的變數
  3. 一直向上搜尋,直到當前模組的全域性變數
  4. 最外層,最後搜尋的,內建(built-in)變數

在任意執行位置,可以將作用域看成是對下面這樣一個名稱空間的搜尋:

除了預設的區域性變數宣告方式,Python 還有globalnonlocal兩種型別的宣告(nonlocal是Python 3.x之後才有,2.7沒有),其中 global 指定的變數直接指向(3)當前模組的全域性變數,而nonlocal則指向(2)最內層之外,global以內的變數。這裡需要強調指向(references and assignments)的原因是,普通的區域性變數對最內層區域性作用域之外只有只讀(read-only)的訪問許可權,比如下面的例子:

這裡丟擲UnboundLocalError,是因為main()函式內部的作用域對於全域性變數x僅有隻讀許可權,想要在main()中對x進行改變,不會影響全域性變數,而是會建立一個新的區域性變數,顯然無法對還未建立的區域性變數直接使用x += 1。如果想要獲得全域性變數的完全引用,則需要global宣告:

Python 閉包

到這裡基本上已經瞭解了 Python 作用域的規則,那麼我們來仿照 JavaScript 寫一個計數器的閉包:

對於還沒有nonlocal關鍵字的 Python 2.7,可以通過一點小技巧來規避區域性作用域只讀的限制:

上面的例子中,inc1()是在全域性環境下執行的,雖然全域性環境是不能向下獲取到inc()中的區域性變數x的,但是我們返回了一個inc()內部的函式inner(),而inner()inc()中的區域性變數是有訪問許可權的。也就是說inner()inc()內的區域性作用域打包送給了inc1inc2,從而使它們各自獨立擁有了一塊封閉起來的作用域,不受全域性變數或者任何其它執行環境的影響,因此稱為閉包。

閉包函式都有一個__closure__屬性,其中包含了它所引用的上層作用域中的變數:

參考

  1. 9.2. Python Scopes and Namespaces
  2. Visualize Python Execution
  3. Wikipedia::Closure

相關文章