在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。
[維基百科::閉包(電腦科學)]
0x02 Python 中的函數語言程式設計 本來也應該包括閉包的概念,但是我覺得閉包更重要的是對作用域(Scope)的理解,因此把它單獨列出來,同時可以理順一下 Python 的作用域規則。
閉包的概念最早出現在函數語言程式設計語言中,後來被一些指令式程式設計語言所借鑑。尤其是在一些函式作為一等公民的語言中,例如JavaScript就經常用到(在JavaScript中函式幾乎可以當做“特等公民”看待),我之前也寫過一篇關於JavaScript閉包的文章(圖解Javascript上下文與作用域),實際上閉包並不是太複雜的概念,但是可以藉助閉包更好地理解不同語言的作用域規則。
名稱空間與作用域
0x00 The Zen of Python的最後一句重點強調名稱空間的概念,我們可以把名稱空間看做一個大型的字典型別(Dict),裡面包含了所有變數的名字和值的對映關係。在 Python 中,作用域實際上可以看做是“在當前上下文的位置,獲取名稱空間變數的規則”。在 Python 程式碼執行的任意位置,都至少存在三層巢狀的作用域:
- 最內層作用域,最早搜尋,包含所有區域性變數(Python 預設所有變數宣告均為區域性變數)
- 所有包含當前上下文的外層函式的作用域,由內而外依次搜尋,這裡包含的是非區域性也非全域性的變數
- 一直向上搜尋,直到當前模組的全域性變數
- 最外層,最後搜尋的,內建(built-in)變數
在任意執行位置,可以將作用域看成是對下面這樣一個名稱空間的搜尋:
1 2 3 4 5 6 |
scopes = { "local": {"locals": None, "non-local": {"locals": None, "global": {"locals": None, "built-in": ["built-ins"]}}}, } |
除了預設的區域性變數宣告方式,Python 還有global
和nonlocal
兩種型別的宣告(nonlocal
是Python 3.x之後才有,2.7沒有),其中 global
指定的變數直接指向(3)當前模組的全域性變數,而nonlocal
則指向(2)最內層之外,global
以內的變數。這裡需要強調指向(references and assignments)的原因是,普通的區域性變數對最內層區域性作用域之外只有只讀(read-only)的訪問許可權,比如下面的例子:
1 2 3 4 5 |
x = 100 def main(): x += 1 print(x) main() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
--------------------------------------------------------------------------- UnboundLocalError Traceback (most recent call last) in () 3 x += 1 4 print(x) ----> 5 main() in main() 1 x = 100 2 def main(): ----> 3 x += 1 4 print(x) 5 main() UnboundLocalError: local variable 'x' referenced before assignment |
這裡丟擲UnboundLocalError
,是因為main()
函式內部的作用域對於全域性變數x
僅有隻讀許可權,想要在main()
中對x
進行改變,不會影響全域性變數,而是會建立一個新的區域性變數,顯然無法對還未建立的區域性變數直接使用x += 1
。如果想要獲得全域性變數的完全引用,則需要global
宣告:
1 2 3 4 5 6 7 8 |
x = 100 def main(): global x x += 1 print(x) main() print(x) # 全域性變數已被改變 |
1 2 |
101 101 |
Python 閉包
到這裡基本上已經瞭解了 Python 作用域的規則,那麼我們來仿照 JavaScript 寫一個計數器的閉包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
""" /* JavaScript Closure example */ var inc = function(){ var x = 0; return function(){ console.log(x++); }; }; var inc1 = inc() var inc2 = inc() """ # Python 3.5 def inc(): x = 0 def inner(): nonlocal x x += 1 print(x) return inner inc1 = inc() inc2 = inc() inc1() inc1() inc1() inc2() |
1 2 3 4 |
1 2 3 1 |
對於還沒有nonlocal
關鍵字的 Python 2.7,可以通過一點小技巧來規避區域性作用域只讀的限制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Python 2.7 def inc(): x = [0] def inner(): x[0] += 1 print(x[0]) return inner inc1 = inc() inc2 = inc() inc1() inc1() inc1() inc2() |
1 2 3 4 |
1 2 3 1 |
上面的例子中,inc1()
是在全域性環境下執行的,雖然全域性環境是不能向下獲取到inc()
中的區域性變數x
的,但是我們返回了一個inc()
內部的函式inner()
,而inner()
對inc()
中的區域性變數是有訪問許可權的。也就是說inner()
將inc()
內的區域性作用域打包送給了inc1
和inc2
,從而使它們各自獨立擁有了一塊封閉起來的作用域,不受全域性變數或者任何其它執行環境的影響,因此稱為閉包。
閉包函式都有一個__closure__
屬性,其中包含了它所引用的上層作用域中的變數:
1 2 |
print(inc1.__closure__[0].cell_contents) print(inc2.__closure__[0].cell_contents) |
1 2 |
[3] [1] |