聊聊 Python Closure

Dboy Liao
10 min readDec 14, 2018

--

今天來聊聊 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 inner
foo = 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 the cell_contents attribute.

那至少,我們可以用 __closure__ 檢視我們對於 Python closure 的理解吧?動手試試吧!

  1. 有 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 msg
x = 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

--

--

Dboy Liao
Dboy Liao

Written by Dboy Liao

Code Writer, Math Enthusiast and Data Scientist, yet.

No responses yet