由於最先用的是 Java 和 C#,寫 Python 的時候天然也把 Python 做用域的想的和原有的一致。python
Python 的做用域變量遵循在大部分狀況下是一致的,但也有例外的狀況。bash
本文着經過遇到的一個做用域的小問題來講說 Python 的做用域app
但也有部分例外的狀況,好比:函數
做用域初版代碼以下ui
a = 1
print(a, id(a)) # 打印 1 4465620064
def func1():
print(a, id(a))
func1() # 打印 1 4465620064複製代碼
做用域初版對應字節碼以下編碼
4 0 LOAD_GLOBAL 0 (print)
3 LOAD_GLOBAL 1 (a)
6 LOAD_GLOBAL 2 (id)
9 LOAD_GLOBAL 1 (a)
12 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
15 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
18 POP_TOP
19 LOAD_CONST 0 (None)
22 RETURN_VALUE複製代碼
PS: 行 4 表示 代碼行數 0 / 3 / 9 ... 不知道是啥,我就先管他叫作條吧 是 load global
PPS: 注意條 3/6 LOAD_GLOBAL 爲從全局變量中加載spa
順手附上本文須要着重理解的幾個指令設計
LOAD_GLOBA : Loads the global named co_names[namei] onto the stack.
LOAD_FAST(var_num) : Pushes a reference to the local co_varnames[var_num] onto the stack.
STORE_FAST(var_num) : Stores TOS into the local co_varnames[var_num].複製代碼
這點彷佛挺符合咱們認知的,那麼,再深一點呢?既然這個變量是能夠 Load 進來的就能夠修改咯?code
然而並非,咱們看做用域第二版對應代碼以下ip
a = 1
print(a, id(a)) # 打印 1 4465620064
def func2():
a = 2
print(a, id(a))
func2() # 打印 2 4465620096複製代碼
一看,WTF, 兩個 a 內存值不同。證實這兩個變量是徹底兩個變量。
做用域第二版對應字節碼以下
4 0 LOAD_CONST 1 (2)
3 STORE_FAST 0 (a)
5 6 LOAD_GLOBAL 0 (print)
9 LOAD_FAST 0 (a)
12 LOAD_GLOBAL 1 (id)
15 LOAD_FAST 0 (a)
18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
21 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
24 POP_TOP
25 LOAD_CONST 0 (None)
28 RETURN_VALUE複製代碼
注意行 4 條 3 (STORE_FAST) 以及行 5 條 9/15 (LOAD_FAST)
這說明了這裏的 a 並非 LOAD_GLOBAL 而來,而是從該函數的做用域 LOAD_FAST 而來。
那咱們在函數體重修改一下 a 值看看。
a = 1
def func3():
print(a, id(a)) # 註釋掉此行不影響結論
a += 1
print(a, id(a))
func3() # 當調用到這裏的時候 local variable 'a' referenced before assignment
# 即 a += 1 => a = a + 1 這裏的第二個 a 報錯鳥複製代碼
3 0 LOAD_GLOBAL 0 (print)
3 LOAD_FAST 0 (a)
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
4 10 LOAD_FAST 0 (a)
13 LOAD_CONST 1 (1)
16 BINARY_ADD
17 STORE_FAST 0 (a)
5 20 LOAD_GLOBAL 0 (print)
23 LOAD_FAST 0 (a)
26 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
29 POP_TOP
30 LOAD_CONST 0 (None)
33 RETURN_VALUE複製代碼
那麼,func3 也就天然而言因爲沒有沒法 LOAD_FAST 對應的 a 變量,則報了引用錯誤。
而後問題來了,a 爲基本類型的時候是這樣的。若是引用類型呢?咱們直接仿照 func3 的實例把 a 改爲 list 類型。以下
a = [1]
def func4():
print(a, id(a)) # 這條注不註釋掉都同樣
a += 1 # 這裏我故意寫錯 按理來講應該是 a.append(1)
print(a, id(a))
func4()
# 當調用到這裏的時候 local variable 'a' referenced before assignment複製代碼
╮(╯▽╰)╭ 看來事情那麼簡單,結果變量 a 依舊是沒法修改。
可按理來講跟應該報下面的錯誤呀
'int' object is not iterable複製代碼
a = [1]
def func5():
print(a, id(a))
a.append(1)
print(a, id(a))
func5()
# [1] 4500243208
# [1, 1] 4500243208複製代碼
這下能夠修改了。看一下字節碼。
3 0 LOAD_GLOBAL 0 (print)
3 LOAD_GLOBAL 1 (a)
6 LOAD_GLOBAL 2 (id)
9 LOAD_GLOBAL 1 (a)
12 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
15 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
18 POP_TOP
4 19 LOAD_GLOBAL 1 (a)
22 LOAD_ATTR 3 (append)
25 LOAD_CONST 1 (1)
28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
31 POP_TOP
5 32 LOAD_GLOBAL 0 (print)
35 LOAD_GLOBAL 1 (a)
38 LOAD_GLOBAL 2 (id)
41 LOAD_GLOBAL 1 (a)
44 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
47 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
50 POP_TOP
51 LOAD_CONST 0 (None)
54 RETURN_VALUE複製代碼
從全局拿來 a 變量,執行 append 方法。
看來這是解釋器遵循了某種變量查找的法則,彷佛就只能從原理上而不是在 CPython 的實現上解釋這個問題了。
查找了一些資料,發現 Python 解釋器在依據 基於 LEGB 準則 (順手吐槽一下不是 LGBT)
LEGB 指的變量查找遵循
StackOverFlow 上 martineau 提供了一個不錯的例子用來講明
x = 100
print("1. Global x:", x)
class Test(object):
y = x
print("2. Enclosed y:", y)
x = x + 1
print("3. Enclosed x:", x)
def method(self):
print("4. Enclosed self.x", self.x)
print("5. Global x", x)
try:
print(y)
except NameError as e:
print("6.", e)
def method_local_ref(self):
try:
print(x)
except UnboundLocalError as e:
print("7.", e)
x = 200 # causing 7 because has same name
print("8. Local x", x)
inst = Test()
inst.method()
inst.method_local_ref()複製代碼
咱們試着用變量查找準則去解釋 第一個例子 的時候,是解釋的通的。
第二個例子,發現函數體內的 a 變量已經不是那個 a 變量了。要是按照這個查找原則的話,彷佛有點說不通了。
但當解釋第三個例子的時候,就徹底說不通了。
a = 1
def func3():
print(a, id(a)) # 註釋掉此行不影響結論
a += 1
print(a, id(a))
func3() # 當調用到這裏的時候 local variable 'a' referenced before assignment
# 即 a += 1 => a = a + 1 這裏的第二個 a 報錯鳥複製代碼
按照個人猜測,這裏的代碼執行可能有兩種狀況:
但若是真的和個人想法接近的話,這兩種狀況均可以執行,除了變量做用域以外仍是有一些其餘的考量。我把這個叫作本地賦值準則 (拍腦殼起的名稱)
通常咱們管這種考量叫作 Python 做者就是以爲這種編碼方式好你愛寫不寫 Python 做者對於變量做用域的權衡。
事實上,當解釋器編譯函數體爲字節碼的時候,若是是一個賦值操做 (list.append 之流不是賦值操做),則會被限定這個變量認爲是一個 local 變量。若是在 local 中找不到,並不向上查找,就報引用錯誤。
這不是 BUG
這不是 BUG
這不是 BUG複製代碼
這是一種設計權衡 Python 認爲 雖然不強求強制聲明類型,但假定被賦值的變量是一個 Local 變量。這樣減小避免動態語言好比 JavaScript 動不動就修改掉了全局變量的坑。
這也就解釋了第四個例子中賦值操做報錯,以及第五個例子 append 爲何能夠正常執行。
若是我偏要勉強呢? 能夠經過 global 和 nonlocal 來 引入模塊級變量 or 上一級變量。
PS: JS 也開始使用 let 進行聲明,小箭頭函數內部賦值查找變量也是向上查找。
ChangeLog: