問題代碼
def create_functions():functions = []for i in range(3):# 創建一個函數,期望捕獲當前循環的i值functions.append(lambda: print(f"My value is: {i}"))return functions# 創建三個函數
f0, f1, f2 = create_functions()# 調用這些函數
f0() # 期望輸出 "My value is: 0"
f1() # 期望輸出 "My value is: 1"
f2() # 期望輸出 "My value is: 2"
但是實際輸出為
My value is: 2
My value is: 2
My value is: 2
類似的,也可以不是用 lambda 表達式,而是使用函數實現閉包
# 依舊有問題
def create_functions():functions = []for i in range(3):def func():print(f"My value is: {i}")functions.append(func)return functions
問題原因解釋
產生這樣問題的原因是:python 閉包捕獲了同一個外部變量 i,并且是通過變量名 i
而非 i
的地址作為索引(這一點很關鍵,雖然實際要比這個復雜,但是可以理解為就是通過名稱確定某個變量的!)
- 如果不是通過變量
i
的字符串名字進行索引,也不會出現這個問題,實際上在for i in range(3)
過程中給你,i
的地址是一直變的 - 所以在最后
f0
、f1
、f2
都用過名字i
來找內存,找到了最后的那個 2 對應的內存地址!
兩種解決方案
方案 1:把值通過變量傳進去,此時閉包引用的是 func 的局部變量 x,而每一個函數實際都是不同的
def create_functions():functions = []for i in range(3):def func(x):return lambda: print(f"My value is: {x}")functions.append(func(i))return functions# 創建三個函數
f0, f1, f2 = create_functions()# 調用這些函數
f0() # 期望輸出 "My value is: 0"
f1() # 期望輸出 "My value is: 1"
f2() # 期望輸出 "My value is: 2"
方案 2:使用函數入參默認值,因為 python 在定義函數默認值時,需要計算出來(這也是另外一個經常出 bug 的問題)
def create_functions():functions = []for i in range(3):def func(x=i):print(f"My value is: {x}")functions.append(func)return functions# 創建三個函數
f0, f1, f2 = create_functions()# 調用這些函數
f0() # 輸出 "My value is: 0"
f1() # 輸出 "My value is: 1"
f2() # 輸出 "My value is: 2"
總結
在產生閉包(尤其是 lambda 表達式這種比較隱蔽時)時,一定要注意閉包中對外部變量的引用是否在發生改變,要仔細思考這些改變是否符合預期
不過,只要知道原理,相信可以很好的處理這些情況