1. 程式人生 > >Python中的變數作用域,LEGB規則和閉包原理

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的異常。