Python中作用域的深入講解
前言
作用域是指變量的生效范圍,例如本地變量、全局變量描述的就是不同的生效范圍。
python的變量作用域的規(guī)則非常簡單,可以說是所有語言中最直觀、最容易理解的作用域。
在開始介紹作用域之前,先拋一個問題:
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
上面的代碼將輸出3、1、1。解釋參見再述作用域規(guī)則。另外,個人建議,本文最后一小節(jié)內(nèi)容盡量理解透徹。
python作用域規(guī)則簡介
它有4個層次的作用域范圍:內(nèi)部嵌套函數(shù)、包含內(nèi)部嵌套函數(shù)的函數(shù)自身、全局作用域、內(nèi)置作用域。上面4個作用域的范圍排序是按照從內(nèi)到外,從小到大排序的。
其中:
- 內(nèi)置作用域是預(yù)先定義好的,在
__builtins__
模塊中。這些名稱主要是一些關(guān)鍵字,例如open、range、quit等 - 全局作用域是文件級別的,或者說是模塊級別的,每個py文件中處于頂層的變量都是全局作用域范圍內(nèi)的變量
- 本地作用域是函數(shù)內(nèi)部屬于本函數(shù)的作用范圍,因為函數(shù)可以嵌套函數(shù),嵌套的內(nèi)層函數(shù)有自身的內(nèi)層范圍
- 嵌套函數(shù)的本地作用域是屬于內(nèi)層函數(shù)的范圍,不屬于外層
所以對于下面這段python代碼來說,如果它處于a.py文件中,且沒有嵌套在其它函數(shù)內(nèi):
X=1 def out1(i): X=2 Y='a' print(X) print(i) def in1(n): print(n) print(X,Y) in1(3) out1(2)
那么:
處于全局作用域范圍的變量有:X、out1
處于out1本地作用域范圍的變量有:i、X、Y、in1
處于嵌套在函數(shù)out1內(nèi)部的函數(shù)in1的本地作用域范圍的變量有:n
注意上面的函數(shù)名out1和in1也是一種變量。
如下圖所示:
搜索規(guī)則
當(dāng)在某個范圍引用某個變量的時候,將從它所在的層次開始搜索變量是否存在,不存在則向外層繼續(xù)搜索。搜索到了,則立即停止。
例如函數(shù)ab()中嵌套了一個函數(shù)cd(),cd()中有一個語句print(x)
,它將首先檢查cd()函數(shù)的本地作用域內(nèi)是否有x,如果沒有則繼續(xù)檢查外部函數(shù)ab()的本地作用域范圍內(nèi)是否有x,如果沒有則再次向外搜索全局范圍內(nèi)的變量x,如果還是沒有,則繼續(xù)搜索內(nèi)置作用域,像"x"這種變量名,在內(nèi)置作用域范圍內(nèi)是不存在的,所以最終沒有搜索到,報錯。如果一開始在cd()中就已經(jīng)找到了變量x,就不會再搜索ab()范圍以及更外層的范圍。
所以,內(nèi)層范圍可以引用外層范圍的變量,外層范圍不包括內(nèi)層范圍的變量。
內(nèi)置作用域
內(nèi)置作用域主要是一些內(nèi)置的函數(shù)名、內(nèi)置的異常等關(guān)鍵字。例如open,range,quit等。
兩種方式可以搜索內(nèi)置作用域:一是直接導(dǎo)入builtins模塊,二是讓python自動搜索。導(dǎo)入builtins模塊會讓內(nèi)置作用域內(nèi)的變量直接置于當(dāng)前文件的全局范圍,自動搜索內(nèi)置作用域則是最后的階段進行搜索。
一般來說無需手動導(dǎo)入builtins模塊,不過可以看看這個模塊中包含了哪些內(nèi)置變量。
>>> import builtins >>> dir(builtins) ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', ............... 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
變量掩蓋和修改規(guī)則
如果在函數(shù)內(nèi)部引用了一個和全局變量同名的變量,且不是重新定義、重新賦值(其實python中沒有變量聲明的概念,只有賦值的概念),那么函數(shù)內(nèi)部引用的是全局變量。
例如,下面的函數(shù)g()中,print函數(shù)中的變量x并未在g()中單獨定義或賦值,所以這個x引用的是全局變量x,它將輸出值3。
x=3 def g(): print(x) # 引用全局變量x
如果函數(shù)內(nèi)部重新賦值了一個和全局變量名稱相同的變量,則這個變量是本地變量,它會掩蓋全局變量。注意是掩蓋而非覆蓋,掩蓋的意思是出了函數(shù)的范圍(函數(shù)退出),全局變量就會恢復(fù)?;蛘邠Q句話說,在函數(shù)內(nèi)部看到的是本地變量x=2
,在函數(shù)外部看到的是全局變量x=3
。
例如:下面的g()中重新聲明了x,這個x稱為g()的本地變量,全局變量x=3
暫時被掩蓋(當(dāng)然,這是對該函數(shù)來說的掩蓋)。
x=3 def g(): x=2 # 定義并賦值本地變量x print(x) # 引用本地變量x
python是一種解釋性語言,讀一行解釋一行,讀了下一行就忘記前一行(詳細見下文)。所以在使用變量之前必須先進行變量的定義(聲明)。
例如下面是錯誤的:
def g(): print(x) x=3 g()
錯誤信息:
UnboundLocalError: local variable 'x' referenced
before assignment
這個很好理解,但是下面和同名的全局變量混合的時候,就不那么容易理解了:
x=1 def g(): print(x) x=2 g()
這里也會報錯,而不是輸出x=1或2。
這里需要解釋一下,雖說python是逐行解釋的。但每個函數(shù)屬于一個區(qū)塊,這個區(qū)塊范圍是一次性解釋的,并不會讀一行忘記一行,而是一直讀,讀完整個區(qū)塊再解釋。所以,讀完整個g()區(qū)塊后,首先就記住了重新定義了本地變量x=2
,于是g()中所有使用變量x的時候,都是本地變量x,所以print(x)中的x也是本地變量,但這違反了使用變量前先賦值的規(guī)則,所以也會報錯。
因此,在函數(shù)內(nèi)修改和全局變量同名的變量前,必須先修改,再使用該變量。所以,上面的代碼中,x=2
必須放在print的前面:
x=1 def g(): x=2 print(x) g()
所以,對于函數(shù)來說,也必須先定義函數(shù),再調(diào)用函數(shù)。下面是錯誤的:
g() def g(): x=2 print(x)
報錯信息:
NameError: name 'g' is not defined
但是下面的代碼中,f()中先調(diào)用了g(),然后才定義g(),為什么能執(zhí)行呢:
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
實際上并非是先調(diào)用了g(),python解釋到def f()區(qū)塊的時候,只是聲明這一個函數(shù),并非調(diào)用這個函數(shù),真正調(diào)用f()的時候是在def g()
區(qū)塊的后面,所以實際上是先聲明完f()和g()之后,再調(diào)用f()和g()的。
但如果把f()放在def f()
和def g()
的中間,將會報錯,因為調(diào)用f()函數(shù)的時候,def g()
還沒解釋到,也就是說g()還沒有聲明。
x=1 def f(): x=3 g() print("f:",x) f() # 報錯 def g(): print("g:",x)
更容易犯錯的一種情況是邊賦值,邊使用。下面的代碼是錯誤的:
x=3 def f1(): x += 3 print(x) f1()
因為x += 3
也是賦值操作,函數(shù)內(nèi)部只要是賦值操作就表示聲明為本地變量。它首先計算x=x+3
右邊的x+3
,然后將結(jié)果賦值給左邊的變量x,但計算x+3
的時候變量x并未定義,所以它是錯誤的。錯誤信息:
UnboundLocalError: local variable 'x' referenced before assignment
關(guān)于全局變量
關(guān)于python中的全局變量:
- 每個py文件(模塊)都有一個自己的全局范圍
- 文件內(nèi)部頂層的,不在def區(qū)塊內(nèi)部的變量,都是全局變量
- def內(nèi)部聲明(賦值)的變量默認是本地變量,要想讓其變成全局變量,需要使用global關(guān)鍵字聲明
- def內(nèi)部如果沒有聲明(賦值)某變量,則引用的這個變量是全局變量
默認情況下,下面f()中的x變量是全局變量:
x=2 def f(): print(x) f() # 輸出2
默認情況下,下面f()中的x變量是本地變量:
x=2 def f(): x=3 print(x) f() # 輸出3 print(x) # 輸出2
global關(guān)鍵字
如果想要在def的內(nèi)部修改全局變量,就需要使用global關(guān)鍵字聲明變量:
x=2 def f(): global x x=3 print(x) f() # 輸出3 print(x) # 輸出3
global可以聲明一個或多個變量為全局變量,多個變量使用逗號隔開,也可以聲明事先不存在的變量為全局變量:
x=2 def f(): global x,y x,y = 3,4 print(x,y) f() print(x,y)
不能global中直接賦值。所以下面的是錯的:
global x=2
注意,global不是聲明變量,在變量賦值之前,變量是一定不存在的,就算是被global修飾了也一樣不存在,所以下面的代碼是錯的。實際上,global有點類似于聲明變量的名稱空間,而非變量。
x=2 def f(): global x,y print(y)
報錯信息:
NameError: name 'y' is not defined
必須在print(y)之前(不能是之后)加上y的賦值語句,才表示它的存在。
x=2 def f(): global x,y y=3 print(y)
global修飾的變量必須在它的賦值之前,所以下面的是錯的,因為y=2首先將它聲明為本地變量了。
def f(): y=2 global y
全局變量的不安全性
考慮下面這個問題:
x=2 def f(): global x x=3 def g(): global x x=4 f()或g() print(x)
這時,函數(shù)f()和g()的調(diào)用順序決定了print輸出的全局變量x的值。因為全局變量是共享的,如果多線程同時執(zhí)行這段代碼,就不知道是誰先誰后修改,導(dǎo)致print(x)的值隨機性。這是多線程不安全特性。因此,如果允許,應(yīng)盡量不要將函數(shù)內(nèi)部的變量修飾為全局變量。
訪問其它模塊中的全局變量
python中一個文件一個模塊,在模塊1中可以導(dǎo)入模塊2中的屬性(例如全局變量)。
例如,b.py文件中:
x=3
a.py文件中:
import b print(b.x) b.x=4
上面的a.py中導(dǎo)入了b.py模塊,通過b.x
可以訪問、修改這個來自于b.py中的全局變量x。
這是極不安全的,因為誰也不知道是否有其他的模塊也在修改b.x
。
所以,沒有人會去直接修改其它模塊的屬性,如果要修改,基本上都會通過類似面向?qū)ο笾械膕etter函數(shù)進行修改。只需在b.py中定義一個函數(shù),以后在其它文件中使用這個函數(shù)修改即可。
b.py文件中:
x=3 def setx(n) global x x=n
a.py文件中:
import b b.setx(54) # 將b.x變量設(shè)置為54
其它訪問全局變量的方法
上面通過import導(dǎo)入模塊文件,就可以獲取這個模塊中屬性的訪問權(quán)。實際上,也可以在當(dāng)前模塊文件中使用import mod_name
導(dǎo)入當(dāng)前模塊,其中mod_name
為當(dāng)前文件名,這樣就可以在函數(shù)內(nèi)部直接訪問全局變量,而無需使用global關(guān)鍵字。
除了import mod_name
可以導(dǎo)入當(dāng)前模塊,使用sys模塊的modules()函數(shù)也可以導(dǎo)入:sys.modules['mod_name']
。
例如,在b.py文件中:
x=3 def f(): global x x += 2 def f1(): x=4 # 本地變量 def f2(): x=4 # 本地變量 import b b.x += 2 # 全局變量 def f3(): x=4 # 本地變量 import sys glob = sys.modules['b'] glob.x += 2 # 全局變量 def test(): print("aaa",x) # 輸出3 f();f1();f2();f3() print("bbb",x) # 輸出9
在a.py文件中:
import b b.test()
nonlocal關(guān)鍵字
當(dāng)函數(shù)進行嵌套的時候,內(nèi)層函數(shù)的作用域是最內(nèi)層的,它的外層是外層函數(shù)的作用域。內(nèi)層函數(shù)和外層函數(shù)的關(guān)系類似于本地作用域與全局作用域的關(guān)系:
(1).內(nèi)層函數(shù)中賦值的變量是屬于內(nèi)層、不屬于外層的本地變量
(2).內(nèi)層函數(shù)中使用的未在當(dāng)前內(nèi)層函數(shù)中賦值的變量是屬于外層、全局的變量
例如,下面的嵌套代碼中,f2()中print(x,y)
的x是屬于外層函數(shù)f1()的本地變量,而y則是屬于內(nèi)層函數(shù)自身的本地變量,外層函數(shù)f1()無法訪問屬于內(nèi)層函數(shù)的y。
x=3 def f1(): x=4 def f2(): y=5 print(x,y) f2() f1()
nonlocal語句可以修飾內(nèi)層函數(shù)中的變量使其成為它上一層函數(shù)的變量。它的用法和global基本相同,修飾多個變量的時候,需要逗號隔開。但和global有一點不同,global修飾的變量可能事先并未存在于全局作用域內(nèi),但nonlocal修飾的變量必須已經(jīng)存在于上層或上上層(或更多層)函數(shù),不能只存在于全局(見下面示例)。
例如下面的代碼片段中嵌套了2次,其中f3()中的x使用nonlocal修飾,使得這個x變成它上一層作用域f2()中的x變量。
x=3 def f1(): x=4 # f1的本地變量 def f2(): x=5 # f2的本地變量 def f3(): nonlocal x # f2的本地變量 print("f3:",x) # 輸出5 x=6 f3() print("f2:",x) # 被修改,輸出6 f2() f1()
上面的代碼將輸出:
f3: 5
f2: 6
如果將上面的f2()中的x=5
刪除,會如何?
x=3 def f1(): x=4 def f2(): def f3(): nonlocal x # f1()的本地 print("f3:",x) # 輸出4 x=6 # 修改f1()的本地 f3() print("f2:",x) # 輸出6 f2() print("f1:",x) # 輸出6 f1()
注意,上面f3()中的nonlocal將x修飾為f1()的本地變量,因為f3()的上一層f2()中沒有變量x,所以f2()繼承了f1()的變量x,使得f3()修改上一層f2()中的變量,等價于修改f1()中的變量x。
但如果把f1()中的x=4
也刪除,那么將報錯,因為nonlocal無法將變量修飾為全局范圍。
所以,nonlocal默認將內(nèi)層函數(shù)中的變量修飾為上一層函數(shù)的作用域范圍,如果上一層函數(shù)中不存在該變量,則修飾為上上層、上上上層直到頂層函數(shù),但不能修飾為全局作用域范圍。
同樣的,只要在內(nèi)層函數(shù)中賦值,就表示聲明這個變量的作用域為內(nèi)層函數(shù)作用域范圍。所以,下面的代碼是錯誤的:
x=3 def f1(): x=4 def f2(): print(x) x=3 f2() f1()
下面的代碼也是錯的:
x=3 def f1(): x=4 def f2(): x += 3 print(x) f2() f1()
錯誤信息:
UnboundLocalError: local variable 'x' referenced before assignment
至于原因,前文已經(jīng)解釋的很清楚。
訪問外層函數(shù)變量的其它方法
在以前的版本中,還沒有nonlocal關(guān)鍵字,這時如果要保存外層函數(shù)的變量,就需要使用函數(shù)參數(shù)默認值的方式定義內(nèi)層函數(shù)。
x=3 def f1(): x=4 def f2(x=x): x += 3 print("f2:",x) x=5 f2() print("f1:",x) f1()
輸出:
f2: 7
f1: 5
上面的f2(x=x)
中,等號右邊的x來自于f1()中x=4
,然后將其賦值給f2()的本地作用域變量x。注意,python的作用域是詞法作用域,函數(shù)區(qū)塊的定義位置決定了它看到的變量。所以,盡管調(diào)用f2()之前再次對x進行了賦值,f2()函數(shù)調(diào)用時,f2(x=x)
等號右邊的x早已經(jīng)賦值給左邊的本地變量x了。它們的關(guān)系如下圖所示:
避免函數(shù)嵌套
一般來說,函數(shù)嵌套都只用于閉包(工廠函數(shù)),而且是結(jié)合匿名函數(shù)(lambda)實現(xiàn)的閉包。其它時候,函數(shù)嵌套一般都可以改寫為非嵌套模式。
例如,下面的嵌套函數(shù):
def f1(): x=3 def f2(): nonlocal x print(x) f2() f1()
可以改寫為:
def f1(): x=3 f2(x) def f2(x): print(x) f1()
當(dāng)函數(shù)位于循環(huán)結(jié)構(gòu)中,且這個函數(shù)引用了循環(huán)控制變量,那么結(jié)果可能會出乎意料。
本來以匿名函數(shù)(lambda)來解釋更清晰,但因為尚未介紹匿名函數(shù),所以這里采用命名函數(shù)為例。
下面的代碼中,將5個函數(shù)作為列表的元素保存到列表list1中。
def f1(): list1 = [] for i in range(5): def n(x): return i+x list1.append(n) return list1 mylist = f1() for i in mylist: print(i) print(mylist[0](2)) print(mylist[2](2))
結(jié)果:
<function f1.<locals>.n at 0x02F93660>
<function f1.<locals>.n at 0x02F934B0>
<function f1.<locals>.n at 0x02F936A8>
<function f1.<locals>.n at 0x02F93738>
<function f1.<locals>.n at 0x02F93780>
6
6
從結(jié)果中可以看到mylist[0](2)
和mylist[2](2)
的執(zhí)行結(jié)果是一樣的,不僅如此,mylist[N](2)
的結(jié)果也全都一樣。換句話說,保存到列表中的各個函數(shù)n()中所引用的循環(huán)控制變量"i"并沒有因為循環(huán)的迭代而改變,而且列表中所有函數(shù)保存的i的值都是循環(huán)的最后一個元素i=4
。
(注:對于此現(xiàn)象,各語言基本都是如此,本節(jié)稍作解釋,真正的本質(zhì)原因在本文的最后一節(jié)做了額外的補充解釋代碼塊細述)。
先看下面的例子:
def f1(): for i in range(5): def n(): print(i) return n f1()()
結(jié)果輸出4??梢姡?code>print(i)的值并沒有隨循環(huán)的迭代過程而改變。
究其原因,是因為def n()
只是函數(shù)的聲明,它不會去查找i的值是多少,所以不會將i的值替換到函數(shù)n()的i變量,而是直接保存變量i的地址,當(dāng)循環(huán)結(jié)束時,i指向最后一個元素i=4的地址。
當(dāng)開始調(diào)用n()的時候,即f1()()
,才會真正開始查找i的值,這時候i指向的正是i=4。
這就像下面的代碼一樣,在還沒有開始調(diào)用f()的時候,f()內(nèi)部的x一直都只是指向它所看見的變量x,而這個x是全局作用域范圍。當(dāng)真正開始調(diào)用f()的時候,才會去定位x的指向。
x=3 def f(): print(x)
回到上面循環(huán)中的嵌套函數(shù),如果要保證循環(huán)的迭代能作用到其內(nèi)部的函數(shù)中,可以采用默認參數(shù)值的方式進行賦值:
def f1(): list1 = [] for i in range(5): def n(x,i=i): return i+x list1.append(n) return list1
上面def n(x,i=i)
中的i=i
是設(shè)置默認參數(shù)值,等號右邊的i是函數(shù)聲明時就查找并替換完成的,所以每次循環(huán)迭代過程中,等號右邊的i都不同,等號左邊的參數(shù)i的默認值就不同。
python的作用域是詞法作用域,這意味著函數(shù)的定義位置決定了它所看見的變量。除了詞法作用域,還有動態(tài)作用域,動態(tài)作用域意味著函數(shù)的調(diào)用位置決定了它所看見的變量。關(guān)于詞法、動態(tài)作用域,本文不多做解釋,想要了解的話,可以參考一文搞懂:詞法作用域、動態(tài)作用域、回調(diào)函數(shù)、閉包
下面是本文開頭的問題:
x=1 def f(): x=3 g() print("f:",x) # 3 def g(): print("g:",x) # 1 f() print("main:",x) # 1
對于python的這段代碼來說,這里有兩個值得注意的地方:
- 調(diào)用函數(shù)之前,理論上要先定義好函數(shù),但這里g()的調(diào)用似乎看上去比g()的定義更先
- f()中調(diào)用g()時,為什么g()輸出的是1而不是3
第一個問題在前文已經(jīng)解釋過了,再解釋一遍:雖然f()里面有g(shù)()的調(diào)用語句,但def f()
只是聲明,但在調(diào)用f()之前,是不會去調(diào)用g()的。所以,只要f()的調(diào)用語句在def g()
之后,就是正確的。
第二個問題,python是詞法作用域,所以:
(1).首先聲明def f()
,在此期間會創(chuàng)建一個本地變量x,并且print("f:",x)
中的x指向這個本地變量;
(2).然后聲明g()
,在此期間,g()的定義語句不在f()內(nèi)部,而是在全局范圍,所以它看見的是x是全局x,所以print("g:",x)
中的x指向全局變量x;
當(dāng)調(diào)用f()的時候,執(zhí)行到g()時,g()中所指向的是全局范圍的x,而非f()段中的x。所以,輸出1。
再看一個嵌套在函數(shù)內(nèi)部的示例:
x=3 def f1(): x=4 def f2(): print(x) x=5 f2() f1() # 輸出5
這里的問題是f2()中的print為什么不輸出4,而是輸出5。
其實也很容易理解,因為def f2()
是定義在f1()內(nèi)部的,所以f2()看見的x是f1()中的x,也就是說print(x)
中的x指向的是f1()中的x。但在調(diào)用f2()之前,重新賦值了x=5
,等到調(diào)用f2()的時候,根據(jù)x的指向,將找到新的x的值。
也就是說,前面的示例中,有兩個獨立的變量x:全局的和f()本地的。后面這個示例中只有一個變量x,屬于f()。
代碼塊可以使得一段python代碼作為一個單元、一個整體執(zhí)行。以下是 官方手冊 的描述。
A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the ‘-c' option) is a code block. The string argument passed to the built-in functions eval() and exec() is a code block.
所以,有以下幾種類型的代碼塊:
- 模塊文件是一個代碼塊
- 函數(shù)體是一個代碼塊
- class的定義是一個代碼塊
- 交互式(python idle)的每一個命令行都是一個獨立的代碼塊
- 腳本文件是一個代碼塊
- 腳本命令是一個代碼塊(python -c "xxx")
- eval()和exec()中的內(nèi)容也都有各自的代碼塊
代碼塊的作用是組織代碼,同時意味著退出代碼區(qū)塊范圍就退出了作用域范圍。例如退出函數(shù)區(qū)塊,就退出了函數(shù)的作用域,使得函數(shù)內(nèi)的本地變量無法被函數(shù)的外界訪問。
此外,python是解釋性語言,讀一行解釋一行,這意味著每讀一行就忘記前一行。但實際上更嚴格的說法是讀一個代碼塊解釋一個代碼塊,這意味著讀代碼塊中的內(nèi)容時,是暫時記住屬于這個代碼塊中所讀內(nèi)容的,讀完整個代碼塊后再以統(tǒng)籌的形式解釋這個代碼塊。
先說明讀一行解釋一行的情況,也就是每一行都屬于一個代碼塊,這個只能通過python的交互式工具idle工具來測試:
>>> x=2000 >>> y=2000 >>> x is y False >>> x=2000;y=2000 >>> x is y True
理論上分號是語句的分隔符,并不會影響結(jié)果。但為什么第一個x is y
為False,而第二個x is y
為True?
首先分析第一個x is y
。由于交互式工具idle中每一個命令都是一個單獨的語句塊,這使得解釋完x=2000
后立刻就忘記了2000這個數(shù)值對象,同時忘記的還有x變量本身。然后再讀取解釋y=2000
,因為不記得剛才解釋的x=2000
,所以會在內(nèi)存中重新創(chuàng)建一個數(shù)值結(jié)構(gòu)用來保存2000這個數(shù)值,然后用y指向它。換句話說,x和y所指向的2000在內(nèi)存中是不同的數(shù)據(jù)對象,所以x is y
為False。
下面的x is y
返回True:
>>> x=2000;y=2000 >>> x is y True
因為python按行解釋,一個命令是一個代碼塊。對于x=2000;y=2000
,python首先讀取這一整行,發(fā)現(xiàn)x和y的數(shù)值對象都是2000,于是做個簡單優(yōu)化,等價于x,y=2000,2000
,這意味著它們屬于一個代碼塊內(nèi),由于都是2000,所以只會在內(nèi)存中創(chuàng)建一個數(shù)據(jù)對象,然后x和y都引用這個數(shù)據(jù)對象。所以,x is y
返回True。
idle工具中每個命令都是獨立的代碼塊,但是py文件卻是一個完整的代碼塊,其內(nèi)還可以嵌套其它代碼塊(如函數(shù)、exec()等)。所以,如果上面的分行賦值語句放在py文件中,得到的結(jié)果將是True。
例如:
x = 2000 y = 2000 print(x is y) # True def f1(): z=2000 z1=2000 print(x is z) # False print(z is z1) # True f1()
python先讀取x=2000
,并在內(nèi)存中創(chuàng)建一個屬于全局作用域的2000數(shù)據(jù)對象,再解釋y=2000的時候,發(fā)現(xiàn)這個全局對象2000已經(jīng)存在了(因為x和y同處于全局代碼塊內(nèi)),所以不會再額外創(chuàng)建新的2000對象。這里反映出來的結(jié)果是"同一個代碼塊內(nèi),雖然仍然是讀一行解釋一行,但在退出這個代碼塊之前,不會忘記這個代碼塊中的內(nèi)容,而且會統(tǒng)籌安排這個代碼塊"。
同理def f1()
內(nèi)的代碼塊,因為z是本地作用域的變量,更標準的是處于不同代碼塊內(nèi),所以會在本地作用域內(nèi)存區(qū)創(chuàng)建新的數(shù)據(jù)對象2000,所以x is z
返回False。根據(jù)前面的解釋,z1 is z
返回True。
再回顧前文多次出現(xiàn)的一個異常:
x = 3 def f1(): print(x) x=4 f1()
報錯信息:
UnboundLocalError: local variable 'x' referenced before assignment
當(dāng)執(zhí)行到def語句的時候,因為def聲明函數(shù),函數(shù)體是一個代碼塊,所以按照代碼塊的方式讀取屬于這個代碼塊中的內(nèi)容。首先讀取print(x)
,但并不會直接解釋,而是會記住它,并繼續(xù)向下讀取,于是讀取x=4,這意味著x是一個本地變量。然后統(tǒng)籌安排整個代碼塊,將print(x)的x認為是本地變量而非全局變量。注意,直到def退出的時候都還沒有進行x的賦值,而是記錄了本地變量x,賦值操作是在函數(shù)調(diào)用的時候進行的。當(dāng)調(diào)用函數(shù)f()的時候,發(fā)現(xiàn)print(x)中的x是本地變量,但因為還沒有賦值,所以報錯。
但是再看下面的,為什么又返回True?
>>> x=256 >>> y=256 >>> x is y True
因為Python在啟動的時候就在內(nèi)存中預(yù)先為常用的較小整數(shù)值(-5到256)創(chuàng)建好了對象,因為它們使用的非常頻繁(有些在python的內(nèi)部已經(jīng)使用了)。所以,對于這個范圍內(nèi)的整數(shù),都是直接引用,不會再在內(nèi)存中額外創(chuàng)建新的數(shù)值對象,所以x is y
總是返回true。甚至,這些小值整數(shù)可以跨作用域:
x = 3 def f1(): y=3 print(x is y) # True f1()
再看前文循環(huán)內(nèi)的函數(shù)的問題。
def f1(): for i in range(5): def n(): print(i) return n f1()()
前面對現(xiàn)象已經(jīng)解釋過,內(nèi)部函數(shù)n()中print(i)的i不會隨循環(huán)的迭代而改變,而是固定的值i=4。
python首先解釋def f1()
的代碼塊,會記錄屬于這個代碼塊作用域內(nèi)的變量i和n,但i和n都不會賦值,也就是說暫時并不知道變量n是一個函數(shù)變量。
同理,當(dāng)需要解釋def n()
代碼塊的時候,將記住這個代碼塊涉及到的變量i,只不過這個變量i是屬于外層函數(shù)的,但不管如何,這個代碼塊記住了i,且記住了它是外部函數(shù)作用域的。
注意,函數(shù)的聲明過程中,所有涉及到變量i的作用域內(nèi)都不會對i進行賦值,僅僅只是保存了這個i變量名,只有在調(diào)用函數(shù)的時候才會進行賦值操作。
當(dāng)開始調(diào)用f1()的時候,開始執(zhí)行函數(shù)體中的代碼,于是開始循環(huán)迭代,且多次聲明函數(shù)n()
,每一次迭代生成的n()都會讓原先已記錄的變量n指向這個新聲明的函數(shù)體(相當(dāng)于賦值的操作,只不過是變量n引用的對象是函數(shù)體結(jié)構(gòu),而不是一般的數(shù)據(jù)對象),由于只是在循環(huán)中聲明函數(shù)n(),并沒有進行調(diào)用,所以不會對n()中的i進行賦值操作。而且,每次循環(huán)迭代都會讓變量n指向新的函數(shù)體,使得先前迭代過程中定義的函數(shù)被丟棄(覆蓋),所以最終只記住了最后一輪循環(huán)時聲明的函數(shù)n(),并且i=4。
當(dāng)調(diào)用f1()()時,表示調(diào)用f1()中返回的函數(shù)n(),直到這個時候才會對n()內(nèi)的i進行賦值,賦值時將搜索它的外層函數(shù)f1()作用域,發(fā)現(xiàn)這個作用域內(nèi)的i指向內(nèi)存中的數(shù)值4,于是最終輸出4。
再看下面的代碼:
def f1(): for i in range(5): def n(): print(i) n() return n f1()
輸出結(jié)果:
0
1
2
3
4
調(diào)用f1()的時候,執(zhí)行循環(huán)的迭代,每次迭代時都會調(diào)用n(),意味著每次迭代都要對n()中的i進行賦值。
另外注意,前面說過,函數(shù)的默認參數(shù)是在函數(shù)聲明時進行賦值的,所以下面的列表L中每個元素所代表的函數(shù),它們的變量i都指向不同的數(shù)值對象。
def f1(): L = [] for i in range(5): def n(i=i): print(i) L.append(n) return L f1()[0]() f1()[1]() f1()[2]() f1()[3]() f1()[4]()
執(zhí)行結(jié)果:
0
1
2
3
4
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
python實現(xiàn)學(xué)生信息管理系統(tǒng)(面向?qū)ο?
這篇文章主要介紹了python實現(xiàn)面向?qū)ο蟀鎸W(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06Python實現(xiàn)Excel和CSV之間的相互轉(zhuǎn)換
通過使用Python編程語言,編寫腳本來自動化Excel和CSV之間的轉(zhuǎn)換過程,可以批量處理大量文件,定期更新數(shù)據(jù),并集成轉(zhuǎn)換過程到自動化工作流程中,本文將介紹如何使用Python 實現(xiàn)Excel和CSV之間的相互轉(zhuǎn)換,需要的朋友可以參考下2024-03-03Python數(shù)據(jù)結(jié)構(gòu)棧實現(xiàn)進制轉(zhuǎn)換簡單示例
眾所周知計算機的內(nèi)存都是以二進制的形式進行數(shù)據(jù)存儲,下面這篇文章主要給大家介紹了關(guān)于Python數(shù)據(jù)結(jié)構(gòu)棧實現(xiàn)進制轉(zhuǎn)換的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2023-02-02利用Python實現(xiàn)網(wǎng)絡(luò)測試的腳本分享
這篇文章主要給大家介紹了關(guān)于利用Python實現(xiàn)網(wǎng)絡(luò)測試的方法,文中給出了詳細的示例代碼供大家參考學(xué)習(xí),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-05-05