摘自<流暢的python> 第七章 函數裝飾器和閉包
實現一個函數(可以不是函數)avg,計算不斷增加的系列值的平均值,效果如下
def avg(...):
pass
avg(10) =>返回10
avg(20) =>返回10+20的平均值15
avg(30) =>返回10+20+30的平均值20
跟Python常見面試題015.請實現一個如下功能的函數有點類似,但又不太一樣
關鍵是你需要有個變數來儲存歷史值
參考程式碼
class Average():
def __init__(self):
self.series = []
def __call__(self, value):
self.series.append(value)
return sum(self.series)/len(self.series)
avg = Average()
print(avg(10))
print(avg(20))
print(avg(30))
avg是個Average的範例
avg有個屬性series,一開始是個空列表
__call__
使得avg物件可以像函數一樣呼叫
呼叫的時候series會保留,因為series只在第一次初始化的時候置為空列表
下面的事情就變得簡單了
參考程式碼
def make_average():
series = []
def averager(value):
series.append(value)
return sum(series)/len(series)
return averager
avg = make_average()
print(avg(10))
print(avg(20))
print(avg(30))
仔細對比2個程式碼,你會發現相似度是極高的
一個是類,一個是函數
類中儲存歷史值的是self.series,函數中的是series區域性變數
類範例能呼叫是實現了__call__
,函數的實現中,avg是make_average()的返回值averager,是個函數名,所以它也能呼叫
閉包closure定義:
外函數
中定義了一個內函數
臨時變數
內函數的參照
以上面的為例
def make_average(): # 外函數
series = [] # 臨時變數(區域性變數)
def averager(value): # 內函數
series.append(value)
return sum(series)/len(series)
return averager # 返回內函數的參照
下面這些話你可能聽的雲裡霧裡的,姑且聽一下。
series 是 make_averager 函數的區域性變數,因為那個函數的定義體中初始化了series:series = []
呼叫 avg(10) 時,make_averager 函數已經返回了,而它的本地作用域也一去不復返了
在 averager 函數中,series 是自由變數(free variable)。這是一個技術術語,指未在本地作用域中繫結的變數
averager 的閉包延伸到那個函數的作用域之外,包含自由變數 series 的繫結
from dis import dis
dis(make_average)
2 0 BUILD_LIST 0
2 STORE_DEREF 0 (series)
3 4 LOAD_CLOSURE 0 (series)
6 BUILD_TUPLE 1
8 LOAD_CONST 1 (<code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>)
10 LOAD_CONST 2 ('make_average.<locals>.averager')
12 MAKE_FUNCTION 8 (closure)
14 STORE_FAST 0 (averager)
6 16 LOAD_FAST 0 (averager)
18 RETURN_VALUE
Disassembly of <code object averager at 0x000002225DD1CBE0, file "<ipython-input-1-a43a8601eedd>", line 3>:
4 0 LOAD_DEREF 0 (series)
2 LOAD_METHOD 0 (append)
4 LOAD_FAST 0 (value)
6 CALL_METHOD 1
8 POP_TOP
5 10 LOAD_GLOBAL 1 (sum)
12 LOAD_DEREF 0 (series)
14 CALL_FUNCTION 1
16 LOAD_GLOBAL 2 (len)
18 LOAD_DEREF 0 (series)
20 CALL_FUNCTION 1
22 BINARY_TRUE_DIVIDE
24 RETURN_VALUE
讀懂上面的,不是人乾的事情,不過你依然有可能
https://docs.python.org/zh-cn/3/library/dis.html#bytecodes
怎麼樣不雲裡霧裡呢
檢視avg.__code__
屬性
[_ for _ in dir(avg.__code__) if _[:2]=='co']
['co_argcount',
'co_cellvars',
'co_code',
'co_consts',
'co_filename',
'co_firstlineno',
'co_flags',
'co_freevars',
'co_kwonlyargcount',
'co_lnotab',
'co_name',
'co_names',
'co_nlocals',
'co_posonlyargcount',
'co_stacksize',
'co_varnames']
官方解釋
屬性 | 描述 |
---|---|
co_argcount | 引數數量(不包括僅關鍵字引數、* 或 ** 引數) |
co_code | 原始編譯位元組碼的字串 |
co_cellvars | 單元變數名稱的元組(通過包含作用域參照) |
co_consts | 位元組碼中使用的常數元組 |
co_filename | 建立此程式碼物件的檔案的名稱 |
co_firstlineno | 第一行在Python原始碼的行號 |
co_flags | CO_* 標誌的點陣圖,詳見 此處 |
co_lnotab | 編碼的行號到位元組碼索引的對映 |
co_freevars | 自由變數的名字組成的元組(通過函數閉包參照) |
co_posonlyargcount | 僅限位置引數的數量 |
co_kwonlyargcount | 僅限關鍵字引數的數量(不包括 ** 引數) |
co_name | 定義此程式碼物件的名稱 |
co_names | 區域性變數名稱的元組 |
co_nlocals | 區域性變數的數量 |
co_stacksize | 需要虛擬機器器堆疊空間 |
co_varnames | 引數名和區域性變數的元組 |
通過__code__
分析
def make_average():
series = []
def averager(value):
series.append(value)
total = sum(series)
return total/len(series)
return averager
avg = make_average()
avg.__code__.co_varnames # 引數名和區域性變數的元組
# ('value', 'total') # value是引數,total是區域性變數名
avg.__code__.co_freevars
# ('series',) # 自由變數的名字組成的元組(通過函數閉包參照)
結合avg.__closure__
avg.__closure__
# (<cell at 0x000002225FA4DC70: list object at 0x000002225EE35600>,)
# 這是個cell物件,list物件
len(avg.__closure__) # 1
avg.__closure__[0].cell_contents # [] 因為你還沒呼叫
avg(10)
avg(20)
avg(30)
avg.__closure__[0].cell_contents # [10, 20, 30] 儲存著真正的值
閉包是一種函數,它會保留定義函數時存在的自由變數的繫結,這樣呼叫函數時,雖然定義作用域不可用了,但是仍能使用那些繫結。
只有巢狀在其他函數中的函數才可能需要處理不在全域性作用域中的外部變數