關于Python閉包的一切
本文轉載自微信公眾號「dongfanger」,作者dongfanger。轉載本文請聯系dongfanger公眾號。
任何把函數當做一等對象的語言,它的設計者都要面對一個問題:作為一等對象的函數在某個作用域中定義,但是可能會在其他作用域中調用,如何處理自由變量?
自由變量(free variable),未在局部作用域中綁定的變量。
為了解決這個問題,Python之父Guido Van Rossum設計了閉包,有如神來之筆,代碼美學盡顯。在討論閉包之前,有必要先了解Python中的變量作用域。
變量作用域
先看一個全局變量和自由變量的示例:
- >>> b = 6
- >>> def f1(a):
- ... print(a)
- ... print(b)
- ...
- >>> f1(3)
- 3
- 6
函數體外的b為全局變量,函數體內的b為自由變量。因為自由變量b綁定到了全局變量,所以在函數f1()中能正確print。
如果稍微改一下,那么函數體內的b就會從自由變量變成局部變量:
- >>> b = 6
- def f1(a):
- ... print(a)
- ... print(b)
- ... b = 9
- ...
- >>> f1(3)
- 3
- Traceback (most recent call last):
- File "<input>", line 1, in <module>
- File "<input>", line 3, in f1
- UnboundLocalError: local variable 'b' referenced before assignment
在函數f1()后面加上b = 9報錯:局部變量b在賦值前進行了引用。
這不是缺陷,而是Python設計:Python不要求聲明變量,而是假定在函數定義體中賦值的變量是局部變量。
如果想讓解釋器把b當做全局變量,那么需要使用global聲明:
- >>> b = 6
- >>> def f1(a):
- ... global b
- ... print(a)
- ... print(b)
- ... b = 9
- ...
- >>> f1(3)
- 3
- 6
閉包
回到文章開頭的自由變量問題,假如有個叫做avg的函數,它的作用是計算系列值的均值,用類實現:
- class Averager():
- def __init__(self):
- self.series = []
- def __call__(self, new_value):
- self.series.append(new_value)
- total = sum(self.series)
- return totle / len(self.series)
- avg = Averager()
- avg(10) # 10.0
- avg(11) # 10.5
- avg(12) # 11.0
類實現不存在自由變量問題,因為self.series是類屬性。但是函數實現,進行函數嵌套時,問題就出現了:
- def make_averager():
- series = []
- def averager(new_value):
- # series是自由變量
- series.append(new_value)
- total = sum(series)
- return totle / len(series)
- return averager
- avg = make_averager()
- avg(10) # 10.0
- avg(11) # 10.5
- avg(12) # 11.0
函數make_averager()在局部作用域中定義了series變量,它的內部函數averager()的自由變量series綁定了這個值。但是在調用avg(10)時,make_averager()函數已經return返回了,它的局部作用域也消失了。沒有閉包的話,自由變量series一定會報錯找不到定義。
那么閉包是怎么做的呢?閉包是一種函數,它會保留定義時存在的自由變量的綁定,這樣調用函數時,雖然定義作用域不可用了,但是仍然能使用那些綁定。
如下圖所示:
閉包會保留自由變量series的綁定,在調用avg(10)時繼續使用這個綁定,即使make_averager()函數的局部作用域已經消失。
nonlocal
把上面示例的需求稍微優化下,只存儲目前的總值和元素個數:
- def make_averager():
- count = 0
- total = 0
- def averager(new_value):
- count += 1
- total += new_value
- return total / count
- return averager
運行后會報錯:局部變量count在賦值前進行了引用。因為count +=1等同于count = count + 1,存在賦值,count就變成局部變量了。total也是如此。
這里如果把count和total通過global關鍵字聲明為全局變量,顯然是不合適的,它們作用域最多只擴展到make_averager()函數內。為了解決這個問題,Python3引入了nonlocal關鍵字聲明:
- def make_averager():
- count = 0
- total = 0
- def averager(new_value):
- nonlocal count, total
- count += 1
- total += new_value
- return total / count
- return averager
nonlocal的作用是把變量標記為自由變量,即使在函數中為變量賦值了,也仍然是自由變量。
注意,對于列表、字典等可變類型來說,添加元素不是賦值,不會隱式創建局部變量。對于數字、字符串、元組等不可變類型以及None來說,賦值會隱式創建局部變量。示例:
- def make_averager():
- # 可變類型
- count = {}
- def averager(new_value):
- print(count) # 成功
- count[new_value] = new_value
- return count
- return averager
可變對象添加元素不是賦值,不會隱式創建局部變量。
- def make_averager():
- # 不可變類型
- count = 1
- def averager(new_value):
- print(count) # 報錯
- count = new_value
- return count
- return averager
count是不可變類型,賦值會隱式創建局部變量,報錯:局部變量count在賦值前進行了引用。
- def make_averager():
- # None
- count = None
- def averager(new_value):
- print(count) # 報錯
- count = new_value
- return count
- return averager
count是None,賦值會隱式創建局部變量,報錯:局部變量count在賦值前進行了引用。
小結
本文先介紹了全局變量、自由變量、局部變量的概念,這是理解閉包的前提。閉包就是用來解決函數嵌套時,自由變量如何處理的問題,它會保留自由變量的綁定,即使局部作用域已經消失。對于不可變類型和None來說,賦值會隱式創建局部變量,把自由變量轉換為局部變量,這可能會導致程序報錯:局部變量在賦值前進行了引用。除了使用global聲明為全局變量外,還可以使用nonlocal聲明把局部變量強制變為自由變量,實現閉包。