此前,我們在介紹 java8 新增的 lambda 表達式時,曾經介紹過“閉包”的概念。
所謂的“閉包”,指的就是可以包含自由變量的代碼塊,代碼塊中包含的自由變量並沒有在定義時綁定任何對象,他們也不是在這個代碼塊內或任何全局上下文中定義的,而是在代碼塊環境中定義的局部變量。 簡單的來說,閉包是一個獨立的代碼塊,但是他可以訪問其定義體之外的非全局變量。 很多語言通過匿名函數來實現閉包特性,著名的 lambda 表達式就是一個典型的閉包的例子。 python 對閉包有著很好的支持。
假設我們有一個方法,每次調用都輸出歷史所有調用傳入參數的總平均數:
>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(39)
20
我們如何來實現呢?下面就是一個閉包的例子:
>>> def make_average():
... series = []
... def avg(value):
... series.append(value)
... return sum(series)/len(series)
... return avg
...
>>> avg = make_average()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(39)
20.0
在 avg = make_average() 語句執行完成後,make_average 方法的棧空間就會被銷毀,而在其棧空間內定義的 series 變量應該會隨著 make_average 方法執行的結束而被銷毀。 但令人意外的是,此後 avg 方法的執行並沒有出錯,其內部對 series 列表的添加並沒有報錯,那麼 series 變量究竟定義在哪裡呢? 此前我們介紹過 python 的作用域,其中提到了 Enclosing 作用域(嵌套函數的外層函數內部) — 嵌套作用域(閉包) python 的名稱空間與作用域
當 python 解釋器看到嵌套函數內部使用了外部該局部變量時,解釋器會將其標記為自由變量,從而不會隨著局部作用域一起被銷毀。
上面的例子我們進一步修改:
>>> def make_average():
... count = total = 0
... def avg(value):
... count += 1
... total += value
... return total / count
... return avg
...
>>> avg = make_average()
>>> avg(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in avg
UnboundLocalError: local variable 'count' referenced before assignment
看上去這個例子與上面閉包的例子沒什麼區別,他將 series 變量改為了保存總和與調用次數的兩個變量,但是卻在調用時報錯,因為外部 count 與 total 隨著 make_average 方法的調用結束而被銷毀了,這又是為什麼呢? 當解釋器看到在嵌套內部的 avg 函數中,對 count 與 total 兩個變量均有賦值行為,於是他們被當做了 avg 方法局部作用域中的變量,而不是自由變量,於是外部的兩個局部變量就被正常銷毀了。 python3 引入了 nonlocal 關鍵字,用於解決這樣的問題:
>>> def make_average():
... count = total = 0
... def avg(value):
... nonlocal count, total
... count += 1
... total += value
... return total / count
... return avg
...
>>> avg = make_average()
>>> avg(10)
10
>>> avg(11)
10.5
>>> avg(39)
20
>>> class Average:
... def __init__(self):
... self.series = []
... def __call__(self, value):
... self.series.append(value)
... return sum(self.series)/len(self.series)
...
>>> avg = Average()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(39)
20.0
這個例子通過類實現了對歷史調用信息的封裝,並通過 __call__ 方法實現了計算邏輯。 通常來說,閉包能夠實現的功能都可以通過類的方式來實現,類也是通常最容易想到的解決方案,那麼,閉包的優勢又體現在哪裡呢? 在 python 中,閉包最重要的使用方式是在裝飾器中,那麼,裝飾器究竟是什麼?閉包與裝飾器結合又能碰撞出什麼樣的火花呢? 我們即將會有一篇文章詳盡介紹裝飾器的用法與原理,敬請期待。