1. 程式人生 > >Python 3 函式自由變數的大坑

Python 3 函式自由變數的大坑

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]