今天來聊聊 Python 的 function closure。
對於初學者來說,closure 可能是個比較難懂的概念。來看看 wiki 上對於 closure 的說法:
a closure (also lexical closure or function closure) is a technique for implementing lexically scoped name binding in a language with first-class functions
嗯…應該有看沒有懂吧?
其實文中所提的 binding 其實白話的來說,就是 variable 在一段程式中是如何被決定它的值會是多少 (lookup)。
而 lexical binding ,根據 EmacsWiki 上的說明,就是每個 scope (function, class, …etc) 會有各自的一張 variable lookup table 。
LEGB
根據 Python.org 的 tutorial 中的說明,當一個變數被使用時,會遵循 LEGB 的規則,也就是 Local、Enclosing、Global 與 Builtins
讓我以下面這段程式為例吧:
glob = 3
def func(x):
y = x + glob
def inner():
return y + 1
return inner, abs
Local,顧名思義,就是在 local variables 裡查找。以上面的例子來說, y
就是 func
的 local variable ,因為 y
是在 func
裡才被定義的。
Global,也就是 global variable ,在上面的例子裡, func
裡用到的 glob
就會是定義在 func
外面的 glob
。
Enclosing,也就是 enclosing scope (別急,等等會解釋)
Builtins,也就是從 builtins
模組裡去找,上面的例子裡就是最後用到的 abs
。
當以上都找不到這個你要的變數時,就會引發 NameError
。
Function Scope
上面的 LGEB 有提到 enclosing scope。在 Python 裡創造一個 scope 的最簡單的方式是 function 。順道一提,在 Python 裡 for
是不會創造一個 scope 的喔!譬如你可以試試下面這串程式碼:
for i in range(10):
pass
print(i)
# 應該會看到 9
p.s 有機會再來聊聊 Python 的 lazy binding 好了,有機會的話….?
言歸正傳。也就是說,在 Python 中當你定義一個 function 時,你就創造了一個 scope ,這個 scope 會影響到你這個 function 裡所有 local 與 non-local 變數會如何被參照。
local 變數我想大家應該不陌生,但這 non-local 又是啥鬼?
這主要是因應 nested scope 而衍生的定義。由於在 Python 裡,所有東西都是 object
,而 object
是 first-class citizen (定義看這邊),所以 function 也是 first-class citizen 。具體來說,由於這樣的設計,你可以在 function 裡定義 function 並回傳 function 。又因為每定義一個 function 就會產生一個 scope ,所以當你在 function 裡又定義 function 時,一個 nested scope 就會被產生 (不負責任白話翻譯: 包在 scope 裡的 scope)
上述 Python.org 的 tutorial 裡就有提到,當走到 LEGB 的 E 時,Python 會從最近的 enclosing scope 向外找起,那這些 enclosing scopes 裡的所有變數,就是所謂的 non-local variable。
舉例說明好了
def outer(a):
b = a
def inner():
c = 3
def inner_inner(b):
r = b+c
return b+c
return inner_inner
return innerfoo = outer(10)
bar = foo()
bar(1) # 4
對 outer
來說, b
是它的 local variable;對 inner
來說, c
是它的 local variable,另一方面雖然沒用到,但是 b
是它的 non-local variable (因為離它最近的 scope 是 outer
所創造出來的 scope,而 b
是在這個 scope 裡的);對 inner_inner
來說, r
是它的 local variable ,值被指定為 b+c
,而這時的 b
並不是在 outer
的 scope 裡被創造的 b
,而是經由參數傳遞進來的,所以也是 local variable 。反觀 c
,則是 inner
的 scope 裡的 c
,對 inner_inner
來說,是屬於 non-local variable。
Under the Hood: __closure__
在科技裡,我總是相信不存在魔法。所以到底 Python 的開發者是如何實現 function closure 的?
老實說我 C 很爛,所以我找不出來 CPython 是怎麼實作 closure 的,但至少,我可以從 Python 官方的 language reference 找答案。
其實也不是什麼稀奇的事,就是 __closure__
這個屬性 (attribute) 。根據 Python 的 Data Model 的定義, __closure__
會是一個唯讀屬性;資料型態是 tuple
,所以是 immutable 的。
那知道這個能幹嘛?又是 read-only 又是 immutable ,好像也不知道能對它做什麼有 (ㄒㄧㄝˊ) 趣 (ㄜˋ) 的事。
說實在的,我也不知道能幹嘛,但 language reference 裡的一句話引起了我的注意:
None
or a tuple of cells that contain bindings for the function’s free variables. See below for information on thecell_contents
attribute.
那至少,我們可以用 __closure__
檢視我們對於 Python closure 的理解吧?動手試試吧!
- 有 non-local variable 就會有
__closure__
?
錯!要有用到才會形成 __closure__
,否則就是 None
。
def foo():
a = 3
def bar():
print("bar")
return bar
bar = foo()
bar.__closure__ is None
# >>> True
這個例子有趣的地方是,你可以發現當一個 function 的內容並沒有用到任何 free variable 時,這時 __closure__
會是 None
。以上面的例子來看,雖然對 bar
來說,有個 a
這個 non-local variable ,但由於 bar
沒有用到它,也因此沒有任何 free variable,所以這時 bar.__closure__
也就還是 None
。
2. 沒有 free variable 就不會有 __closure__
?
錯!如果 inner scope 有用到 free variable ,就會被包含到 outer scope 裡。
def foo():
a = 3
def bar():
def hell():
return a
return hell
return bar
bar = foo()
bar.__closure__ is None
# False
bar.__closure__
# (<cell at 0x109d54408: int object at 0x10787eaf0>,)
bar().__closure__ == bar.__closure__
# True
bar.__closure__[0].cell_contents
# 3
在這個例子裡,雖然 bar
沒有用到任何 free variable ,但是 hell
透過 nested scope 取得了 foo
裡的 a
,間接影響了 bar.__closure__
。
3. Bonus Question
以下程式碼有錯誤嗎?
def foo():
def bar():
return bar # Error?
return bar
bar = foo() # Error?
會有 error 嗎?
如果沒有的話,想想為什麼吧 : ) (Hint: LEGB)
學 closure 要幹嘛?
如果你的背景是 JavaScript 來的,我想這個問題應該是在明顯不過,當然非常實用!舉例來說,在 JavaScript 裡不乏看到這樣的程式碼:
(function(msg){
var x = 3;
function inner(){
// do things with x and msg
}
return inner;
})('my awesome string');
也就是利用匿名函數的 scope 進行 variable 的隔離 (例如上面的 x
)。
很不幸的,Python 的 lambda
只能有一句 statement ,很難做到等價的 JavaScript code ,只能用 def
(但這樣就不匿名了 Q___Q)。
但除此之外,常見的就會是可帶參數的 decorator 實作。假設你現在必須寫個 function 的 decorator ,他會讓函數在回傳 None
時,改回傳另一個你指定的值,那簡單的實作可能會長這樣:
def return_default(value):
def deco(func):
def wrapped(*args, **kwargs):
ret = func(*args, **kwargs)
if ret is None:
ret = value
return ret
return wrapped
return deco
接者你可以這樣使用 return_default
:
@return_default(10)
def at_least_10(x):
if x >= 10:
return x@return_default("python")
def greeting(msg):
return msgx = at_least_10(3) # 10
msg = greeting(None) # "python"
簡單的說, return_default
這個 decorator 利用了 closure ,在自己的 scope 裡定義了一個 inner scope 把 value
放在裡面的 deco
的 closure 裡,才能做到這種夾帶參數的 decorator 。
如果我們用剛剛學過的 __closure__
檢視,就能發現:
return_default(10).__closure__
# (<cell at 0x109d54528: int object at 0x10787ebd0>,)
return_default(10).__closure__[0].cell_contents
# 10
return_default("hello").__closure__[0].cell_contents
# 'hello'
如果這樣還說服不了你,那讓我用幾個有名的 open source project 的 code 當例子吧:
好啦,Python 的 function closure 就簡單介紹到這裡啦。
Happy Python Programming!
References
- https://en.wikipedia.org/wiki/Closure_(computer_programming)
- https://www.emacswiki.org/emacs/DynamicBindingVsLexicalBinding#toc2
- https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces
- https://gist.github.com/DmitrySoshnikov/700292
- http://stupidpythonideas.blogspot.com/2015/12/how-lookup-works.html
- https://docs.python.org/3/reference/datamodel.html