python作用域的理解-理解Python的UnboundLocalError(Python的作用域)
今天寫代碼碰到一個百思不得解為什么會出錯的代碼,簡化如下:
1
2
3
4
5
6
7
x=10
deffunc():
ifsomething_true():
x=20
print(x)
func()
意圖很明顯,首先我定義了一個全局的x,在函數中,如果有特殊需要,就重新重新賦值一下x,否則就使用全局的x。
可以這段代碼在運行的時候拋出這個Error:
UnboundLocalError: local variable "a’ referenced before assignment
研究了一番,覺得挺有意思的。而且這是一個比較常見的問題,在Stack Overflow的Python tag下面基本上是個周經問題。
出現賦值就是局部變量!
基本的原理很簡單,在Python FAQ中提到了:
在Python中,如果變量僅僅是被引用而沒有被賦值過,那么默認被視作全局變量。如果一個變量在函數中被賦值過,那么就被視作局部變量。
在Effective Python也提到過:
Python是這樣處理賦值操作的:如果變量在當前的作用域已經定義過,那么就會變成賦值操作。否則的話會在當前的作用域定義一個新的變量。(Assigning a value to a variable works differently. If the variable is already defined in the current scope, then it will just take on the new value. If the variable doesn’t exist in the current scope, then Python treats the assignment as a variable definition. The scope of the newly defined variable is the function that contains the assignment.)
重點強調一下,這里的被賦值過,指的是在函數體內任何地方被賦值過。無論是否會被執行到(比如在if語句中),甚至是變量引用之后再賦值(參考下面的代碼),都被作為“被賦值過”,都變成了局部變量。
1
2
3
4
5
6
7
In[26]:deftest_assignment():
...:printx
...:x=20
...:
In[27]:test_assignment()
UnboundLocalError:localvariable"x"referencedbeforeassignment
其實到這里這個問題的答案已經出來了,只要是在函數體內被賦值過,那么變量就是local的,任何賦值之前的操作都會出現一個RuntimeError。下面會深入解釋一下。
賦值操作的編譯過程(原理)
Python文檔中有關賦值語句提到:
Assignment of an object to a single target is recursively defined as follows. If the target is an identifier (name):
If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace.
Otherwise: the name is bound to the object in the current global namespace.
就是說,如果賦值操作的變量沒有用global聲明,那么就將這個name綁定到局部名字空間,否則就綁定到全局名字空間。
我們可以使用symtable這個lib驗證一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
importsymtable
code="""
x = 10
def foo():
x += 1
print(x)
"""
table=symtable.symtable(code,"","exec")
foo_namespace=table.lookup("foo").get_namespace()
sym_x=foo_namespace.lookup("x")
print(sym_x.get_name())# x
print(sym_x.is_local())# True
可以看到,x變量確實被綁定到了局部。使用dis庫可以看到編譯的代碼:
1
2
3
4
5
6
7
8
9
10
11
350LOAD_FAST0(x)
3LOAD_CONST1(1)
6INPLACE_ADD
7STORE_FAST0(x)
3610LOAD_GLOBAL0(print)
13LOAD_FAST0(x)
16CALL_FUNCTION1
19POP_TOP
20LOAD_CONST0(None)
23RETURN_VALUE
其中,LOAD_FAST是從local的stack中讀取變量名(LOAD_FAST對之后字節碼的優化很重要)。由此可以看到,的確是在局部變量找x沒有找到(前面并沒有STORE_FAST操作),引發了UnboundLocalError。
所以我的理解是:Python編譯建立抽象語法樹的時候,根據語法書建立符號表,從語法書的函數體內決定符號是local的還是global(是否出現assignment語句),然后在編譯其他語句生成字節碼。
那么既然這樣,為什么要等到運行的時候才報錯,而不是編譯的時候就報錯呢?
參考下面的代碼:
1
2
3
4
5
6
x=10
deffoo():
ifsomething_true():
x=1
x+=1
print(x)
如果something_true(),x的賦值就會執行,那么代碼不會拋異常。但是編譯器并不會知道這個賦值語句會不會執行。換句話說,函數體內出現了賦值語句,但是Python編譯過程無法得知賦值語句會不會執行到的。所以只要出現了賦值語句,就將變量視為局部。至于會不會出現未賦值就使用(UnboundLocalError),就運行看看了。
Python為什么要這樣處理?
這并不是缺陷,而是一個設計選擇。
Python不要求聲明變量,但是假設函數體內定義的變量都是局部變量。這比JavaScript好多了,JavaScript也不要求聲明變量,但是如果不是var聲明的變量,可能在不知情的情況下修改了全局變量。《Fluent Python》7.4
(PS:ES6的 let?也有了類似的機制,叫做“temporal dead zone”,參考)
這應該很好理解,試想一下,如果在函數中引用了一個函數內不存在的變量,后面又進行了賦值。而Python將這個變量當做全局變量,那么可能隱式地給你覆蓋了全局變量。這如果是debug起來肯定是個噩夢。
這種設計選擇正是提現了Python的設計哲學:“Explicit is better than implicit.”
解決方法
前面已經提到了,顯示地指定使用global就可以,這樣即使出現賦值,也不會產生作為local的變量,而是去改變global的變量。
但是依然存在一個問題:
1
2
3
4
5
6
7
8
9
defexternal():
x=10
definternal():
globalx
x+=1
print(x)
internal()
external()
external的x既不是local,也不是global。這種情況應該使用Python3的nonlocal。這樣Python不會在當前的作用域找x,會去上一層找。
可惜Python2不支持nonlocal,但是我們可以使用“閉包”來解決。其實思想就是,如果我們無法改變不可變的對象,就將這個對象變成可以改變的對象。
1
2
3
4
5
6
7
8
defexternal():
x=[10]
definternal():
x[0]+=1
print(x)
internal()
external()# [11]
如上代碼,x不是一個不可改變的int,而是一個可變的list對象。這樣x[0] += 1就會變成一個賦值操作,而不會申請新的變量。
2018年8月30日更新:最近讀《代碼之髓》這本書,對 Python 的作用域以及它的行為有了新的理解。Python 是靜態作用域的,而且變量無須聲明,賦值即聲明。像 Perl,JavaScript 這樣的需要是需要聲明的,比如帶上 var?就是局部變量,否則就是全局變量。Python 這種賦值即聲明的方式,好處就是我們在寫的時候很爽,一般都符合我們的直覺。缺點就是在嵌套函數內部如果想要賦值,那么依據“賦值即聲明”,我們就會創建新的變量,而不會去修改外部函數的變量。
與之類似的語言是 Ruby,在 Ruby 中同樣“賦值即聲明”,不過行為卻與 Python 恰恰相反。
在 Ruby 中,如果嵌套方法,外部方法的變量在內部方法中依然視作外部方法的變量;如果在內部方法創建變量,那么只會存在于內部方法中,不會影響外部方法。通俗一點,如果內部方法對一個變量 a?賦值的話,如果外部方法有 a?,那么外部方法的 a?的值會被修改;否則,會在內部方法創建一個 a,內部方法結束之后,a?就不存在了。一下代碼為例:
1
2
3
4
5
6
7
8
deffoo()
x="old"
lambda{x="new";y="new"}.call# 相當于一個內部方法
px# 外部的 x 被修改成 "new"
py# y 是內部方法創建的,不存在于外部方法中,報錯
end
foo
參考資料
Effective Python:Item 15: Know How Closures Interact with Variable Scope
總結
以上是生活随笔為你收集整理的python作用域的理解-理解Python的UnboundLocalError(Python的作用域)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机械硬盘电脑的价格(机械硬盘报价大全)
- 下一篇: win8配置_《FIFA 20》PC配置