我們從一個測試開始。下面這個函數的功能是什么?
def foo(lst): a = 0 for i in lst: a += i b = 1 for t in lst: b *= i return a, b
如果你覺得它的功能是“計算lst中所有元素的和與積”,不要沮喪。通常很難發現這里的錯誤。如果在大堆真實的代碼中發現了這個錯誤就非常厲害了。——當你不知道這是一個測試時,很難發現這個錯誤。
這里的錯誤是在第二個循環體中使用了i而不是t。等下,這到底是怎么工作的?i在第一個循環外應該是不可見的? [1]哦,不。事實上,Python正式聲明過,為for循環目標(loop target)定義的名稱(更嚴格的正式名稱為“索引變量”)能泄露到外圍函數范圍。因此下面的代碼:
for i in [1, 2, 3]: passprint(i)
這段代碼是有效的,可以打印出3。在本文中,我想探討一下為什么會這樣,為什么它不太可能改變,以及將它作為一顆追蹤子彈來挖掘CPython編輯器中一些有趣的部分。
順便說一句,如果你不相信這種行為可能會導致真正的問題,考慮這個代碼片斷:
def foo(): lst = [] for i in range(4): lst.append(lambda: i) print([f() for f in lst])
如果你期待上面的代碼能打印出[0,1,2,3],你的期望會落空的,它會打印出[3,3,3,3];因為在foo的作用域內只有一個i,這個i就是所有的lambda所捕獲的。
官方說明
Python參考文檔中的for循環部分明確地記錄了這種行為:
for循環將變量賦值到目標列表中。……當循環結束時,賦值列表中的變量不會被刪除,但如果序列是空的,它們將不會被賦值給所有的循環。
注意最后一句,讓我們試試:
for i in []: passprint(i)
的確,上面的代碼拋出NameError異常。稍后,我們將看到這是Python虛擬機執行字節碼方式的必然結果。
為什么會是這樣
其實我問過Guido van Rossum有關這個執行行為的原因,他很慷慨地告訴了我其中的一些歷史背景(感謝Guido!)。這樣執行代碼的動機是保持Python獲得變量和作用域的簡單性,而不訴諸于hacks(例如在循環完成后,刪除定義在該循環中的所有變量——想想它可能引發的異常)或更復雜的作用域規則。
Python的作用域規則非常簡單、優雅:模塊、類以及函數的代碼塊可引入作用域。在函數體內,變量從它們定義到代碼塊結束(包括嵌套的代碼塊如嵌套函數)都是可見的。當然,對于局部變量、全局變量(以及其他nonlocal變量)其規則略有不同。不過,這和我們的討論沒有太多關系。
這里最重要的一點是:最內層的可能作用域是一個函數體。不是一個for循環體。不是一個with代碼塊。Python與其他編程語言不同(例如C及其后代語言),在函數水平下沒有嵌套詞法作用域。
新聞熱點
疑難解答