Python 3 函式自由變數的大坑
阿新 • • 發佈:2018-11-11
Python中函式是一個物件, 和整數,字串等物件有很多相似之處,例如可以作為其他函式的引數或返回物件, Python中的函式還可以攜帶自由變數, 兩者無疑極大增進了Python的表達力.
但是Python函式自由變數的內部機制和列表解析或for迴圈結合使用時卻暗藏殺機:
#---CASE 1 fs = map(lambda i:(lambda j: i*j),range(6)) print([f(2) for f in fs]) #---CASE 2 fs = [lambda j:i*j for i in range(6)] print([f(2) for f in fs]) #---CASE 3 fs = [] for i in range(6): fs.append(lambda j:i*j) if i==3: break print([f(2) for f in fs]) #---CASE 4 fs = [(lambda i:lambda j:i*j)(i) for i in range(6)] print([f(2) for f in fs])
結果:
[0, 2, 4, 6, 8, 10] [10, 10, 10, 10, 10, 10] [6, 6, 6, 6] [0, 2, 4, 6, 8, 10]
可以通過下面這個簡單的測試來分析Python函式在執行時是如何確定自由變數的值的:
i = 1 def f(j): return i*j print(f(2)) # ---> 2 i = 2 print(f(2)) # ---> 4 def g(): i = 3 def f(j): return i*j return f f = g() print(f(2)) # ---> 6 i = 100 print(f(2)) # ---> 6
可見,當 函式f在*定義時*, Python不會記錄自由變數'i'對應什麼物件, 只會告訴f, 你有一個自由變數, 它的名字叫 'i'.
接著, 當函式f在*執行時*, Python告訴f:
(1) 空間上: 你需要在你被*定義時*的外層namespace裡面去查詢i對應的物件, 假設這個namespace為X.
(2) 時間上: 是在你*當前執行時*, X 裡面的 i 對應的物件.
上面那個簡單測試中的 i = 2 之後, f(2)隨之也返回4也能反映了這一點.
CASE 2和3 也是如此, fs裡面每個函式對應的自由變數i在*定義時*都是迴圈變數i, 因此*執行時*都是對應迴圈結束或跳出時i所指物件.
而 CASE 1和4為什麼能如願發生變化呢? 這是因為函式對應的自由變數i不再是迴圈變數i, 而是外層lambda函式*執行時*,迴圈變數i所指物件在其棧上的拷貝, 由於每次呼叫外層lambda時i所指物件都不相同, 因此每個函式的自由變數也會指向不同的物件.
最後, 列表解析裡面的作用域是一個全新的作用域, 而普通的for迴圈則有所不同. 例如:
#---CASE 2 fs = [lambda j:i*j for i in range(6)] print([f(2) for f in fs]) i = 4 print([f(2) for f in fs]) #---CASE 3 fs = [] for i in range(6): fs.append(lambda j:i*j) print([f(2) for f in fs]) i = 4 print([f(2) for f in fs])
結果是:
[10, 10, 10, 10, 10, 10] [10, 10, 10, 10, 10, 10] [10, 10, 10, 10, 10, 10] [8, 8, 8, 8, 8, 8]