python作用域的理解-理解Python的UnboundLocalError(Python的作用域)
今天寫(xiě)代碼碰到一個(gè)百思不得解為什么會(huì)出錯(cuò)的代碼,簡(jiǎn)化如下:
1
2
3
4
5
6
7
x=10
deffunc():
ifsomething_true():
x=20
print(x)
func()
意圖很明顯,首先我定義了一個(gè)全局的x,在函數(shù)中,如果有特殊需要,就重新重新賦值一下x,否則就使用全局的x。
可以這段代碼在運(yùn)行的時(shí)候拋出這個(gè)Error:
UnboundLocalError: local variable "a’ referenced before assignment
研究了一番,覺(jué)得挺有意思的。而且這是一個(gè)比較常見(jiàn)的問(wèn)題,在Stack Overflow的Python tag下面基本上是個(gè)周經(jīng)問(wèn)題。
出現(xiàn)賦值就是局部變量!
基本的原理很簡(jiǎn)單,在Python FAQ中提到了:
在Python中,如果變量?jī)H僅是被引用而沒(méi)有被賦值過(guò),那么默認(rèn)被視作全局變量。如果一個(gè)變量在函數(shù)中被賦值過(guò),那么就被視作局部變量。
在Effective Python也提到過(guò):
Python是這樣處理賦值操作的:如果變量在當(dāng)前的作用域已經(jīng)定義過(guò),那么就會(huì)變成賦值操作。否則的話(huà)會(huì)在當(dāng)前的作用域定義一個(gè)新的變量。(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.)
重點(diǎn)強(qiáng)調(diào)一下,這里的被賦值過(guò),指的是在函數(shù)體內(nèi)任何地方被賦值過(guò)。無(wú)論是否會(huì)被執(zhí)行到(比如在if語(yǔ)句中),甚至是變量引用之后再賦值(參考下面的代碼),都被作為“被賦值過(guò)”,都變成了局部變量。
1
2
3
4
5
6
7
In[26]:deftest_assignment():
...:printx
...:x=20
...:
In[27]:test_assignment()
UnboundLocalError:localvariable"x"referencedbeforeassignment
其實(shí)到這里這個(gè)問(wèn)題的答案已經(jīng)出來(lái)了,只要是在函數(shù)體內(nèi)被賦值過(guò),那么變量就是local的,任何賦值之前的操作都會(huì)出現(xiàn)一個(gè)RuntimeError。下面會(huì)深入解釋一下。
賦值操作的編譯過(guò)程(原理)
Python文檔中有關(guān)賦值語(yǔ)句提到:
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.
就是說(shuō),如果賦值操作的變量沒(méi)有用global聲明,那么就將這個(gè)name綁定到局部名字空間,否則就綁定到全局名字空間。
我們可以使用symtable這個(gè)lib驗(yàn)證一下:
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變量確實(shí)被綁定到了局部。使用dis庫(kù)可以看到編譯的代碼:
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對(duì)之后字節(jié)碼的優(yōu)化很重要)。由此可以看到,的確是在局部變量找x沒(méi)有找到(前面并沒(méi)有STORE_FAST操作),引發(fā)了UnboundLocalError。
所以我的理解是:Python編譯建立抽象語(yǔ)法樹(shù)的時(shí)候,根據(jù)語(yǔ)法書(shū)建立符號(hào)表,從語(yǔ)法書(shū)的函數(shù)體內(nèi)決定符號(hào)是local的還是global(是否出現(xiàn)assignment語(yǔ)句),然后在編譯其他語(yǔ)句生成字節(jié)碼。
那么既然這樣,為什么要等到運(yùn)行的時(shí)候才報(bào)錯(cuò),而不是編譯的時(shí)候就報(bào)錯(cuò)呢?
參考下面的代碼:
1
2
3
4
5
6
x=10
deffoo():
ifsomething_true():
x=1
x+=1
print(x)
如果something_true(),x的賦值就會(huì)執(zhí)行,那么代碼不會(huì)拋異常。但是編譯器并不會(huì)知道這個(gè)賦值語(yǔ)句會(huì)不會(huì)執(zhí)行。換句話(huà)說(shuō),函數(shù)體內(nèi)出現(xiàn)了賦值語(yǔ)句,但是Python編譯過(guò)程無(wú)法得知賦值語(yǔ)句會(huì)不會(huì)執(zhí)行到的。所以只要出現(xiàn)了賦值語(yǔ)句,就將變量視為局部。至于會(huì)不會(huì)出現(xiàn)未賦值就使用(UnboundLocalError),就運(yùn)行看看了。
Python為什么要這樣處理?
這并不是缺陷,而是一個(gè)設(shè)計(jì)選擇。
Python不要求聲明變量,但是假設(shè)函數(shù)體內(nèi)定義的變量都是局部變量。這比JavaScript好多了,JavaScript也不要求聲明變量,但是如果不是var聲明的變量,可能在不知情的情況下修改了全局變量。《Fluent Python》7.4
(PS:ES6的 let?也有了類(lèi)似的機(jī)制,叫做“temporal dead zone”,參考)
這應(yīng)該很好理解,試想一下,如果在函數(shù)中引用了一個(gè)函數(shù)內(nèi)不存在的變量,后面又進(jìn)行了賦值。而Python將這個(gè)變量當(dāng)做全局變量,那么可能隱式地給你覆蓋了全局變量。這如果是debug起來(lái)肯定是個(gè)噩夢(mèng)。
這種設(shè)計(jì)選擇正是提現(xiàn)了Python的設(shè)計(jì)哲學(xué):“Explicit is better than implicit.”
解決方法
前面已經(jīng)提到了,顯示地指定使用global就可以,這樣即使出現(xiàn)賦值,也不會(huì)產(chǎn)生作為local的變量,而是去改變global的變量。
但是依然存在一個(gè)問(wèn)題:
1
2
3
4
5
6
7
8
9
defexternal():
x=10
definternal():
globalx
x+=1
print(x)
internal()
external()
external的x既不是local,也不是global。這種情況應(yīng)該使用Python3的nonlocal。這樣Python不會(huì)在當(dāng)前的作用域找x,會(huì)去上一層找。
可惜Python2不支持nonlocal,但是我們可以使用“閉包”來(lái)解決。其實(shí)思想就是,如果我們無(wú)法改變不可變的對(duì)象,就將這個(gè)對(duì)象變成可以改變的對(duì)象。
1
2
3
4
5
6
7
8
defexternal():
x=[10]
definternal():
x[0]+=1
print(x)
internal()
external()# [11]
如上代碼,x不是一個(gè)不可改變的int,而是一個(gè)可變的list對(duì)象。這樣x[0] += 1就會(huì)變成一個(gè)賦值操作,而不會(huì)申請(qǐng)新的變量。
2018年8月30日更新:最近讀《代碼之髓》這本書(shū),對(duì) Python 的作用域以及它的行為有了新的理解。Python 是靜態(tài)作用域的,而且變量無(wú)須聲明,賦值即聲明。像 Perl,JavaScript 這樣的需要是需要聲明的,比如帶上 var?就是局部變量,否則就是全局變量。Python 這種賦值即聲明的方式,好處就是我們?cè)趯?xiě)的時(shí)候很爽,一般都符合我們的直覺(jué)。缺點(diǎn)就是在嵌套函數(shù)內(nèi)部如果想要賦值,那么依據(jù)“賦值即聲明”,我們就會(huì)創(chuàng)建新的變量,而不會(huì)去修改外部函數(shù)的變量。
與之類(lèi)似的語(yǔ)言是 Ruby,在 Ruby 中同樣“賦值即聲明”,不過(guò)行為卻與 Python 恰恰相反。
在 Ruby 中,如果嵌套方法,外部方法的變量在內(nèi)部方法中依然視作外部方法的變量;如果在內(nèi)部方法創(chuàng)建變量,那么只會(huì)存在于內(nèi)部方法中,不會(huì)影響外部方法。通俗一點(diǎn),如果內(nèi)部方法對(duì)一個(gè)變量 a?賦值的話(huà),如果外部方法有 a?,那么外部方法的 a?的值會(huì)被修改;否則,會(huì)在內(nèi)部方法創(chuàng)建一個(gè) a,內(nèi)部方法結(jié)束之后,a?就不存在了。一下代碼為例:
1
2
3
4
5
6
7
8
deffoo()
x="old"
lambda{x="new";y="new"}.call# 相當(dāng)于一個(gè)內(nèi)部方法
px# 外部的 x 被修改成 "new"
py# y 是內(nèi)部方法創(chuàng)建的,不存在于外部方法中,報(bào)錯(cuò)
end
foo
參考資料
Effective Python:Item 15: Know How Closures Interact with Variable Scope
總結(jié)
以上是生活随笔為你收集整理的python作用域的理解-理解Python的UnboundLocalError(Python的作用域)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 机械硬盘电脑的价格(机械硬盘报价大全)
- 下一篇: python人工智能_人工智能福利丨Py