0x00 前言
因?yàn)樽钤缬玫氖?Java 和 C#,寫 Python 的時候自然也把 Python 作用域的想的和原有的一致。
Python 的作用域變量遵循在大部分情況下是一致的,但也有例外的情況。
本文著通過遇到的一個作用域的小問題來說說 Python 的作用域
0x01 作用域的幾個實(shí)例
但也有部分例外的情況,比如:
1.1 第一個例子
作用域第一版代碼如下
a = 1
print(a, id(a)) # 打印 1 4465620064
def func1():
print(a, id(a))
func1() # 打印 1 4465620064
作用域第一版對應(yīng)字節(jié)碼如下
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 表示 代碼行數(shù) 0 / 3 / 9 ... 不知道是啥,我就先管他叫做條吧 是 load global
PPS: 注意條 3/6 LOAD_GLOBAL 為從全局變量中加載
順手附上本文需要著重理解的幾個指令
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].
這點(diǎn)似乎挺符合我們認(rèn)知的,那么,再深一點(diǎn)呢?既然這個變量是可以 Load 進(jìn)來的就可以修改咯?
1.2 第二個例子
然而并不是,我們看作用域第二版對應(yīng)代碼如下
a = 1
print(a, id(a)) # 打印 1 4465620064
def func2():
a = 2
print(a, id(a))
func2() # 打印 2 4465620096
一看,WTF, 兩個 a 內(nèi)存值不一樣。證明這兩個變量是完全兩個變量。
作用域第二版對應(yīng)字節(jié)碼如下
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 而來,而是從該函數(shù)的作用域 LOAD_FAST 而來。
1.3 第三個例子
那我們在函數(shù)體重修改一下 a 值看看。
a = 1
def func3():
print(a, id(a)) # 注釋掉此行不影響結(jié)論
a += 1
print(a, id(a))
func3() # 當(dāng)調(diào)用到這里的時候 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 對應(yīng)的 a 變量,則報了引用錯誤。
然后問題來了,a 為基本類型的時候是這樣的。如果引用類型呢?我們直接仿照 func3 的實(shí)例把 a 改成 list 類型。如下
1.4 第四個例子
a = [1]
def func4():
print(a, id(a)) # 這條注不注釋掉都一樣
a += 1 # 這里我故意寫錯 按理來說應(yīng)該是 a.append(1)
print(a, id(a))
func4()
# 當(dāng)調(diào)用到這里的時候 local variable 'a' referenced before assignment
╮(╯▽╰)╭ 看來事情那么簡單,結(jié)果變量 a 依舊是無法修改。
可按理來說跟應(yīng)該報下面的錯誤呀
'int' object is not iterable
1.5 第五個例子
a = [1]
def func5():
print(a, id(a))
a.append(1)
print(a, id(a))
func5()
# [1] 4500243208
# [1, 1] 4500243208
這下可以修改了??匆幌伦止?jié)碼。
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 變量,執(zhí)行 append 方法。
0x02 作用域準(zhǔn)則以及本地賦值準(zhǔn)則
2.1 作用域準(zhǔn)則
看來這是解釋器遵循了某種變量查找的法則,似乎就只能從原理上而不是在 CPython 的實(shí)現(xiàn)上解釋這個問題了。
查找了一些資料,發(fā)現(xiàn) Python 解釋器在依據(jù) 基于 LEGB 準(zhǔn)則 (順手吐槽一下不是 LGBT)
LEGB 指的變量查找遵循
- Local
- Enclosing-function locals
- Global
- Built-In
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()
我們試著用變量查找準(zhǔn)則去解釋 第一個例子 的時候,是解釋的通的。
第二個例子,發(fā)現(xiàn)函數(shù)體內(nèi)的 a 變量已經(jīng)不是那個 a 變量了。要是按照這個查找原則的話,似乎有點(diǎn)說不通了。
但當(dāng)解釋第三個例子的時候,就完全說不通了。
a = 1
def func3():
print(a, id(a)) # 注釋掉此行不影響結(jié)論
a += 1
print(a, id(a))
func3() # 當(dāng)調(diào)用到這里的時候 local variable 'a' referenced before assignment
# 即 a += 1 => a = a + 1 這里的第二個 a 報錯鳥
按照我的猜想,這里的代碼執(zhí)行可能有兩種情況:
- 當(dāng)代碼執(zhí)行到第三行的時候可能是向從 local 找 a, 發(fā)現(xiàn)沒有,再找 Enclosing-function 發(fā)現(xiàn)沒有,最后應(yīng)該在 Global 里面找到才是。注釋掉第三行的時候也是同理。
- 當(dāng)代碼執(zhí)行到第三行的時候可能是向下從 local 找 a, 發(fā)現(xiàn)有,然后代碼執(zhí)行,結(jié)束。
但如果真的和我的想法接近的話,這兩種情況都可以執(zhí)行,除了變量作用域之外還是有一些其他的考量。我把這個叫做本地賦值準(zhǔn)則 (拍腦袋起的名稱)
一般我們管這種考量叫做 Python 作者就是覺得這種編碼方式好你愛寫不寫 Python 作者對于變量作用域的權(quán)衡。
事實(shí)上,當(dāng)解釋器編譯函數(shù)體為字節(jié)碼的時候,如果是一個賦值操作 (list.append 之流不是賦值操作),則會被限定這個變量認(rèn)為是一個 local 變量。如果在 local 中找不到,并不向上查找,就報引用錯誤。
這不是 BUG
這不是 BUG
這不是 BUG
這是一種設(shè)計權(quán)衡 Python 認(rèn)為 雖然不強(qiáng)求強(qiáng)制聲明類型,但假定被賦值的變量是一個 Local 變量。這樣減少避免動態(tài)語言比如 JavaScript 動不動就修改掉了全局變量的坑。
這也就解釋了第四個例子中賦值操作報錯,以及第五個例子 append 為什么可以正常執(zhí)行。
如果我偏要勉強(qiáng)呢? 可以通過 global 和 nonlocal 來 引入模塊級變量 or 上一級變量。
PS: JS 也開始使用 let 進(jìn)行聲明,小箭頭函數(shù)內(nèi)部賦值查找變量也是向上查找。
0xEE 參考鏈接
ChangeLog:
- 2017-11-20 從原有筆記中抽取本文整理而成