8.1 不理解 Python 的作用域規則
Python 的作用域基於所謂的 LEGB 規則, 這是以下詞語的縮寫:
-
Local
(在函數內以任何方式賦值的名稱(def
或lambda
中),但未在該函數中聲明為全局); Enclosing
(在任何靜態包圍函數(def
或lambda
)的局部作用域中的名稱,從內到外);Global
(在模塊文件的頂層賦值的名稱,或在文件中的def
中通過執行global
語句);-
Built-in
(在內建名稱模塊中預設賦值的名稱:open
,range
,SyntaxError
, 等等)。
看起來很簡單,對吧?
然而,Python 的工作方式有一些細微之處, 這讓我們進入了 Python 編程中的複雜問題。 請考慮以下示例:
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
問題出在哪裡?
上述錯誤發生是因為,當你在作用域中給變量賦值時
,Python
自動認為它是這個作用域的局部變量
,並隱藏任何在更高層作用域中具有相同名稱的變量。
因此,很多人在修改函數體中添加賦值語句以後得到 UnboundLocalError
時感到驚訝,因為代碼之前是正常工作的。
這種特性在使用列表時特別讓開發者困惑。請考慮下一個示例:
lst = [1, 2, 3]
def foo1():
lst.append(5) # 這工作正常...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... 但這就崩了!
foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
為什麼 foo2
崩了,而 foo1
正常工作?
答案和之前的例子相同,不過普遍認為,這裡的情況更為微妙。foo1
沒有對 lst
使用賦值運算符,而 foo2
使用了。記住,lst += [5]
實際上只是 lst = lst + [5]
的簡寫,我們看到,我們試圖給 lst
賦值(因此 Python 假定它在局部作用域中)。然而,我們想要賦給 lst
的值是基於 lst
本身的(同樣,現在假定它在局部作用域中),而它還未被定義。所以我們得到了錯誤。
8.2 在迭代列表時修改列表
下一段代碼中的問題應該很明顯:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
for i in range(len(numbers)):
if odd(numbers[i]):
del numbers[i] # BAD: Deleting item from a list while iterating over it
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
在迭代列表或數組時刪除其中的元素是 Python 中的一個問題,任何有經驗的軟體開發人員都很清楚這一點。但是,儘管上述範例可能相當明顯,即使是有經驗的開發人員也可能在更複雜的代碼中犯這個錯誤。
幸運的是,Python 包含許多優雅的程式設計範式,如果正確使用可以大大簡化和優化代碼。由此產生的一個愉快的結果是,編寫更簡單代碼的可能性降低了在迭代時意外刪除列表元素的可能性。
其中一種範式是列表生成。另外,理解列表生成的工作原理對於避免這個具體問題特別有用,如下面這個替代實現所示,它運行的非常好:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # 只篩選新的元素
print(numbers)
# [0, 2, 4, 6, 8]
重要!
這裡不會有新的列表對象被賦值。使用
numbers[:]
是對所有列表元素進行批量賦值。
8.3 不理解 Python 如何在閉包中繫結變量
請考慮以下示例:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #返回函數列表!
for multiplier in create_multipliers():
print(multiplier(2))
你可能預期以下輸出:
0
2
4
6
8
但是實際上你會得到:
8
8
8
8
8
驚喜!
這是因為 延遲綁定 在 Python 中,這意味著在閉包中使用的變量的值是在內部函數調用時查找的。
因此,在上面的代碼中,每當調用任何返回的函數時,i
的值是在調用時在周圍作用域中查找的(而那時循環已經結束,因此 i
已經被賦予最終結果——值 4)。
這個常見的 Python 問題的解決方案如下:
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0
# 2
# 4
# 6
# 8
瞧!我們在這裡使用默認參數來生成匿名函數以實現所需的行為。有些人會稱這個解決方案為優雅。有些人會稱之為微妙。一些人討厭這類東西。但如果你是 Python 開發人員,理解這點是很重要的,無論如何。
GO TO FULL VERSION