Python中的變數作用域,LEGB規則和閉包原理
問題來源
最近看到了一個python程式題,就三行程式碼,卻思考了很久才考慮明白,決定分享一下。
def num(): return [lambda x:i*x for i in range(4)] print([m(2) for m in num()])
預計結果為:0, 2, 4, 6
實際輸出為:6, 6, 6, 6
思路分析
其實把上面的程式碼拆分一下,等價於下面的程式碼
def func(): fun_lambda_list = [] for i in range(4): def lamb(x):return x*i fun_lambda_list.append(lamb) return fun_lambda_list
我們再把上面的程式碼加兩行print輸出,讓結果看的更加明顯:
PS:locals() 函式會以字典型別返回當前位置的全部區域性變數。
def func(): fun_lambda_list = [] for i in range(4): def lamb(x): print('Lambda函式中 i {} 名稱空間為:{}:'.format(i, locals()))return x*i fun_lambda_list.append(lamb) print('外層函式 I 為:{} 名稱空間為:{}'.format(i, locals())) return fun_lambda_list fl = func() fl[0](1) fl[1](1) fl[2](1) fl[3](1)
我們會發現,列印的結果為:
外層函式 I 為:0 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x00000116B6837488>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x00000116B6837488>], 'i': 0} 外層函式 I 為:1 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x00000116B6837510>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x00000116B6837488>, <function func.<locals>.lambda_ at 0x00000116B6837510>], 'i': 1} 外層函式 I 為:2 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x00000116B6837598>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x00000116B6837488>, <function func.<locals>.lambda_ at 0x00000116B6837510>, <function func.<locals>.lambda_ at 0x00000116B6837598>], 'i': 2} 外層函式 I 為:3 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x00000116B6837620>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x00000116B6837488>, <function func.<locals>.lambda_ at 0x00000116B6837510>, <function func.<locals>.lambda_ at 0x00000116B6837598>, <function func.<locals>.lambda_ at 0x00000116B6837620>], 'i': 3} Lambda函式中 i 3 名稱空間為:{'x': 1, 'i': 3}: Lambda函式中 i 3 名稱空間為:{'x': 1, 'i': 3}: Lambda函式中 i 3 名稱空間為:{'x': 1, 'i': 3}: Lambda函式中 i 3 名稱空間為:{'x': 1, 'i': 3}:
可以發現:四次迴圈中外層函式名稱空間中的 i 從 0-->1-->2-->3 最後固定為3,而在此過程中內嵌函式lamb函式中因為沒有定義 i 所以只有lamb函式動態執行時,在自己名稱空間中找不到 i 才去外層函式複製 i = 3 過來,結果就是所有lamb函式的 i 都為 3,導致得不到預計輸出結果:0,2,4,6 只能得到 6,6,6,6。
解決辦法
變閉包作用域為區域性作用域
def func(): fun_lambda_list = [] for i in range(4): def lambda_(x,i=i): print('Lambda函式中 i {} 名稱空間為:{}:'.format(i, locals())) return x*i fun_lambda_list.append(lambda_) print('外層函式 I 為:{} 名稱空間為:{}'.format(i, locals())) return fun_lambda_list fl = func() fl[0](1) fl[1](1) fl[2](1) fl[3](1)View Code
外層函式 I 為:0 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x0000021F12227488>, 'i': 0, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x0000021F12227488>]} 外層函式 I 為:1 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x0000021F12227510>, 'i': 1, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x0000021F12227488>, <function func.<locals>.lambda_ at 0x0000021F12227510>]} 外層函式 I 為:2 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x0000021F12227598>, 'i': 2, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x0000021F12227488>, <function func.<locals>.lambda_ at 0x0000021F12227510>, <function func.<locals>.lambda_ at 0x0000021F12227598>]} 外層函式 I 為:3 名稱空間為:{'lambda_': <function func.<locals>.lambda_ at 0x0000021F12227620>, 'i': 3, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x0000021F12227488>, <function func.<locals>.lambda_ at 0x0000021F12227510>, <function func.<locals>.lambda_ at 0x0000021F12227598>, <function func.<locals>.lambda_ at 0x0000021F12227620>]} Lambda函式中 i 0 名稱空間為:{'i': 0, 'x': 1}: Lambda函式中 i 1 名稱空間為:{'i': 1, 'x': 1}: Lambda函式中 i 2 名稱空間為:{'i': 2, 'x': 1}: Lambda函式中 i 3 名稱空間為:{'i': 3, 'x': 1}:輸出結果
所以,再回到最開始的那段程式碼。lambda x: x*i
為內層(嵌)函式,他的名稱空間中只有 {'x': 1} 沒有 i ,所以執行時會向外層函式(這兒是列表解析式函式 [ ])的名稱空間中請求 i 。而當列表解析式執行時,列表解析式名稱空間中的 i 經過迴圈依次變化為 0-->1-->2-->3 最後固定為 3 ,所以當 lambda x: x*i
內層函式執行時,去外層函式取 i 每次都只能取到 3。
將程式碼改成下面這樣輸出就會變成0,2,4,6.
def num(): return [lambda x,i=i:i*x for i in range(4)] print([m(2) for m in num()])
LEGB規則
只有函式、類、模組會產生作用域,程式碼塊不會產生作用域。作用域按照變數的定義位置可以劃分為4類:
Local(函式內部)區域性作用域 Enclosing(巢狀函式的外層函式內部)巢狀作用域(閉包) Global(模組全域性)全域性作用域 Built-in(內建)內建作用域
python直譯器查詢變數時,會按照順序依次查詢區域性作用域--->巢狀作用域--->全域性作用域--->內建作用域,在任意一個作用域中找到變數則停止查詢,所有作用域查詢完成沒有找到對應的變數,則丟擲 NameError: name 'xxxx' is not defined的異常。