溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Python中作用域的深入講解

發(fā)布時(shí)間:2020-09-21 11:02:17 來(lái)源:腳本之家 閱讀:116 作者:駿馬金龍 欄目:開(kāi)發(fā)技術(shù)

前言

作用域是指變量的生效范圍,例如本地變量、全局變量描述的就是不同的生效范圍。

python的變量作用域的規(guī)則非常簡(jiǎn)單,可以說(shuō)是所有語(yǔ)言中最直觀、最容易理解的作用域。

在開(kāi)始介紹作用域之前,先拋一個(gè)問(wèn)題:

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。解釋參見(jiàn)再述作用域規(guī)則。另外,個(gè)人建議,本文最后一小節(jié)內(nèi)容盡量理解透徹。

python作用域規(guī)則簡(jiǎn)介

它有4個(gè)層次的作用域范圍:內(nèi)部嵌套函數(shù)、包含內(nèi)部嵌套函數(shù)的函數(shù)自身、全局作用域、內(nèi)置作用域。上面4個(gè)作用域的范圍排序是按照從內(nèi)到外,從小到大排序的。

Python中作用域的深入講解

其中:

  • 內(nèi)置作用域是預(yù)先定義好的,在__builtins__模塊中。這些名稱主要是一些關(guān)鍵字,例如open、range、quit等
  • 全局作用域是文件級(jí)別的,或者說(shuō)是模塊級(jí)別的,每個(gè)py文件中處于頂層的變量都是全局作用域范圍內(nèi)的變量
  • 本地作用域是函數(shù)內(nèi)部屬于本函數(shù)的作用范圍,因?yàn)楹瘮?shù)可以嵌套函數(shù),嵌套的內(nèi)層函數(shù)有自身的內(nèi)層范圍
  • 嵌套函數(shù)的本地作用域是屬于內(nèi)層函數(shù)的范圍,不屬于外層

所以對(duì)于下面這段python代碼來(lái)說(shuō),如果它處于a.py文件中,且沒(méi)有嵌套在其它函數(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也是一種變量。

如下圖所示:

Python中作用域的深入講解

搜索規(guī)則

當(dāng)在某個(gè)范圍引用某個(gè)變量的時(shí)候,將從它所在的層次開(kāi)始搜索變量是否存在,不存在則向外層繼續(xù)搜索。搜索到了,則立即停止。

例如函數(shù)ab()中嵌套了一個(gè)函數(shù)cd(),cd()中有一個(gè)語(yǔ)句print(x),它將首先檢查cd()函數(shù)的本地作用域內(nèi)是否有x,如果沒(méi)有則繼續(xù)檢查外部函數(shù)ab()的本地作用域范圍內(nèi)是否有x,如果沒(méi)有則再次向外搜索全局范圍內(nèi)的變量x,如果還是沒(méi)有,則繼續(xù)搜索內(nèi)置作用域,像"x"這種變量名,在內(nèi)置作用域范圍內(nèi)是不存在的,所以最終沒(méi)有搜索到,報(bào)錯(cuò)。如果一開(kāi)始在cd()中就已經(jīng)找到了變量x,就不會(huì)再搜索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òng)搜索。導(dǎo)入builtins模塊會(huì)讓內(nèi)置作用域內(nèi)的變量直接置于當(dāng)前文件的全局范圍,自動(dòng)搜索內(nèi)置作用域則是最后的階段進(jìn)行搜索。

一般來(lái)說(shuō)無(wú)需手動(dòng)導(dǎo)入builtins模塊,不過(guò)可以看看這個(gè)模塊中包含了哪些內(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)部引用了一個(gè)和全局變量同名的變量,且不是重新定義、重新賦值(其實(shí)python中沒(méi)有變量聲明的概念,只有賦值的概念),那么函數(shù)內(nèi)部引用的是全局變量。

例如,下面的函數(shù)g()中,print函數(shù)中的變量x并未在g()中單獨(dú)定義或賦值,所以這個(gè)x引用的是全局變量x,它將輸出值3。

x=3
def g():
 print(x) # 引用全局變量x

如果函數(shù)內(nèi)部重新賦值了一個(gè)和全局變量名稱相同的變量,則這個(gè)變量是本地變量,它會(huì)掩蓋全局變量。注意是掩蓋而非覆蓋,掩蓋的意思是出了函數(shù)的范圍(函數(shù)退出),全局變量就會(huì)恢復(fù)?;蛘邠Q句話說(shuō),在函數(shù)內(nèi)部看到的是本地變量x=2,在函數(shù)外部看到的是全局變量x=3。

例如:下面的g()中重新聲明了x,這個(gè)x稱為g()的本地變量,全局變量x=3暫時(shí)被掩蓋(當(dāng)然,這是對(duì)該函數(shù)來(lái)說(shuō)的掩蓋)。

x=3
def g():
 x=2 # 定義并賦值本地變量x
 print(x) # 引用本地變量x

python是一種解釋性語(yǔ)言,讀一行解釋一行,讀了下一行就忘記前一行(詳細(xì)見(jiàn)下文)。所以在使用變量之前必須先進(jìn)行變量的定義(聲明)。

例如下面是錯(cuò)誤的:

def g():
 print(x)
 x=3
g()

錯(cuò)誤信息:

UnboundLocalError: local variable 'x' referenced
before assignment

這個(gè)很好理解,但是下面和同名的全局變量混合的時(shí)候,就不那么容易理解了:

x=1
def g():
 print(x)
 x=2
g()

這里也會(huì)報(bào)錯(cuò),而不是輸出x=1或2。

這里需要解釋一下,雖說(shuō)python是逐行解釋的。但每個(gè)函數(shù)屬于一個(gè)區(qū)塊,這個(gè)區(qū)塊范圍是一次性解釋的,并不會(huì)讀一行忘記一行,而是一直讀,讀完整個(gè)區(qū)塊再解釋。所以,讀完整個(gè)g()區(qū)塊后,首先就記住了重新定義了本地變量x=2,于是g()中所有使用變量x的時(shí)候,都是本地變量x,所以print(x)中的x也是本地變量,但這違反了使用變量前先賦值的規(guī)則,所以也會(huì)報(bào)錯(cuò)。

因此,在函數(shù)內(nèi)修改和全局變量同名的變量前,必須先修改,再使用該變量。所以,上面的代碼中,x=2必須放在print的前面:

x=1
def g():
 x=2
 print(x)
g()

所以,對(duì)于函數(shù)來(lái)說(shuō),也必須先定義函數(shù),再調(diào)用函數(shù)。下面是錯(cuò)誤的:

g()
def g():
 x=2
 print(x)

報(bào)錯(cuò)信息:

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

實(shí)際上并非是先調(diào)用了g(),python解釋到def f()區(qū)塊的時(shí)候,只是聲明這一個(gè)函數(shù),并非調(diào)用這個(gè)函數(shù),真正調(diào)用f()的時(shí)候是在def g()區(qū)塊的后面,所以實(shí)際上是先聲明完f()和g()之后,再調(diào)用f()和g()的。

但如果把f()放在def f()def g()的中間,將會(huì)報(bào)錯(cuò),因?yàn)檎{(diào)用f()函數(shù)的時(shí)候,def g()還沒(méi)解釋到,也就是說(shuō)g()還沒(méi)有聲明。

x=1
def f():
 x=3
 g()
 print("f:",x)

f() # 報(bào)錯(cuò)

def g():
 print("g:",x) 

更容易犯錯(cuò)的一種情況是邊賦值,邊使用。下面的代碼是錯(cuò)誤的:

x=3

def f1():
 x += 3
 print(x)
f1()

因?yàn)?code>x += 3也是賦值操作,函數(shù)內(nèi)部只要是賦值操作就表示聲明為本地變量。它首先計(jì)算x=x+3右邊的x+3,然后將結(jié)果賦值給左邊的變量x,但計(jì)算x+3的時(shí)候變量x并未定義,所以它是錯(cuò)誤的。錯(cuò)誤信息:

UnboundLocalError: local variable 'x' referenced before assignment

關(guān)于全局變量

關(guān)于python中的全局變量:

  • 每個(gè)py文件(模塊)都有一個(gè)自己的全局范圍
  • 文件內(nèi)部頂層的,不在def區(qū)塊內(nèi)部的變量,都是全局變量
  • def內(nèi)部聲明(賦值)的變量默認(rèn)是本地變量,要想讓其變成全局變量,需要使用global關(guān)鍵字聲明
  • def內(nèi)部如果沒(méi)有聲明(賦值)某變量,則引用的這個(gè)變量是全局變量

默認(rèn)情況下,下面f()中的x變量是全局變量:

x=2
def f():
 print(x)
f() # 輸出2

默認(rèn)情況下,下面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可以聲明一個(gè)或多個(gè)變量為全局變量,多個(gè)變量使用逗號(hào)隔開(kāi),也可以聲明事先不存在的變量為全局變量:

x=2
def f():
 global x,y
 x,y = 3,4
 print(x,y)
f()
print(x,y)

不能global中直接賦值。所以下面的是錯(cuò)的:

global x=2

注意,global不是聲明變量,在變量賦值之前,變量是一定不存在的,就算是被global修飾了也一樣不存在,所以下面的代碼是錯(cuò)的。實(shí)際上,global有點(diǎn)類(lèi)似于聲明變量的名稱空間,而非變量。

x=2
def f():
 global x,y
 print(y)

報(bào)錯(cuò)信息:

NameError: name 'y' is not defined

必須在print(y)之前(不能是之后)加上y的賦值語(yǔ)句,才表示它的存在。

x=2
def f():
 global x,y
 y=3
 print(y)

global修飾的變量必須在它的賦值之前,所以下面的是錯(cuò)的,因?yàn)閥=2首先將它聲明為本地變量了。

def f():
 y=2
 global y

全局變量的不安全性

考慮下面這個(gè)問(wèn)題:

x=2
def f():
 global x
 x=3

def g():
 global x
 x=4

f()或g()
print(x)

這時(shí),函數(shù)f()和g()的調(diào)用順序決定了print輸出的全局變量x的值。因?yàn)槿肿兞渴枪蚕淼?,如果多線程同時(shí)執(zhí)行這段代碼,就不知道是誰(shuí)先誰(shuí)后修改,導(dǎo)致print(x)的值隨機(jī)性。這是多線程不安全特性。因此,如果允許,應(yīng)盡量不要將函數(shù)內(nèi)部的變量修飾為全局變量。

訪問(wèn)其它模塊中的全局變量

python中一個(gè)文件一個(gè)模塊,在模塊1中可以導(dǎo)入模塊2中的屬性(例如全局變量)。

例如,b.py文件中:

x=3

a.py文件中:

import b
print(b.x)
b.x=4

上面的a.py中導(dǎo)入了b.py模塊,通過(guò)b.x可以訪問(wèn)、修改這個(gè)來(lái)自于b.py中的全局變量x。

這是極不安全的,因?yàn)檎l(shuí)也不知道是否有其他的模塊也在修改b.x。

所以,沒(méi)有人會(huì)去直接修改其它模塊的屬性,如果要修改,基本上都會(huì)通過(guò)類(lèi)似面向?qū)ο笾械膕etter函數(shù)進(jìn)行修改。只需在b.py中定義一個(gè)函數(shù),以后在其它文件中使用這個(gè)函數(shù)修改即可。

b.py文件中:

x=3
def setx(n)
 global x
 x=n

a.py文件中:

import b
b.setx(54) # 將b.x變量設(shè)置為54

其它訪問(wèn)全局變量的方法

上面通過(guò)import導(dǎo)入模塊文件,就可以獲取這個(gè)模塊中屬性的訪問(wèn)權(quán)。實(shí)際上,也可以在當(dāng)前模塊文件中使用import mod_name導(dǎo)入當(dāng)前模塊,其中mod_name為當(dāng)前文件名,這樣就可以在函數(shù)內(nèi)部直接訪問(wèn)全局變量,而無(wú)需使用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ù)進(jìn)行嵌套的時(shí)候,內(nèi)層函數(shù)的作用域是最內(nèi)層的,它的外層是外層函數(shù)的作用域。內(nèi)層函數(shù)和外層函數(shù)的關(guān)系類(lèi)似于本地作用域與全局作用域的關(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()無(wú)法訪問(wèn)屬于內(nèi)層函數(shù)的y。

x=3

def f1():
 x=4
 def f2():
 y=5
 print(x,y)
 f2()
f1()

nonlocal語(yǔ)句可以修飾內(nèi)層函數(shù)中的變量使其成為它上一層函數(shù)的變量。它的用法和global基本相同,修飾多個(gè)變量的時(shí)候,需要逗號(hào)隔開(kāi)。但和global有一點(diǎn)不同,global修飾的變量可能事先并未存在于全局作用域內(nèi),但nonlocal修飾的變量必須已經(jīng)存在于上層或上上層(或更多層)函數(shù),不能只存在于全局(見(jiàn)下面示例)。

例如下面的代碼片段中嵌套了2次,其中f3()中的x使用nonlocal修飾,使得這個(gè)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刪除,會(huì)如何?

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()的本地變量,因?yàn)閒3()的上一層f2()中沒(méi)有變量x,所以f2()繼承了f1()的變量x,使得f3()修改上一層f2()中的變量,等價(jià)于修改f1()中的變量x。

但如果把f1()中的x=4也刪除,那么將報(bào)錯(cuò),因?yàn)閚onlocal無(wú)法將變量修飾為全局范圍。

所以,nonlocal默認(rèn)將內(nèi)層函數(shù)中的變量修飾為上一層函數(shù)的作用域范圍,如果上一層函數(shù)中不存在該變量,則修飾為上上層、上上上層直到頂層函數(shù),但不能修飾為全局作用域范圍。

同樣的,只要在內(nèi)層函數(shù)中賦值,就表示聲明這個(gè)變量的作用域?yàn)閮?nèi)層函數(shù)作用域范圍。所以,下面的代碼是錯(cuò)誤的:

x=3
def f1():
 x=4
 def f2():
 print(x)
 x=3
 f2()
f1()

下面的代碼也是錯(cuò)的:

x=3
def f1():
 x=4
 def f2():
 x += 3
 print(x)
 f2()
f1()

錯(cuò)誤信息:

UnboundLocalError: local variable 'x' referenced before assignment

至于原因,前文已經(jīng)解釋的很清楚。

訪問(wèn)外層函數(shù)變量的其它方法

在以前的版本中,還沒(méi)有nonlocal關(guān)鍵字,這時(shí)如果要保存外層函數(shù)的變量,就需要使用函數(shù)參數(shù)默認(rèn)值的方式定義內(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)中,等號(hào)右邊的x來(lái)自于f1()中x=4,然后將其賦值給f2()的本地作用域變量x。注意,python的作用域是詞法作用域,函數(shù)區(qū)塊的定義位置決定了它看到的變量。所以,盡管調(diào)用f2()之前再次對(duì)x進(jìn)行了賦值,f2()函數(shù)調(diào)用時(shí),f2(x=x)等號(hào)右邊的x早已經(jīng)賦值給左邊的本地變量x了。它們的關(guān)系如下圖所示:

Python中作用域的深入講解

避免函數(shù)嵌套

一般來(lái)說(shuō),函數(shù)嵌套都只用于閉包(工廠函數(shù)),而且是結(jié)合匿名函數(shù)(lambda)實(shí)現(xiàn)的閉包。其它時(shí)候,函數(shù)嵌套一般都可以改寫(xiě)為非嵌套模式。

例如,下面的嵌套函數(shù):

def f1():
 x=3
 def f2():
 nonlocal x
 print(x)
 f2()
f1()

可以改寫(xiě)為:

def f1():
 x=3
 f2(x)

def f2(x):
 print(x)

f1()

循環(huán)內(nèi)部的函數(shù)

當(dāng)函數(shù)位于循環(huán)結(jié)構(gòu)中,且這個(gè)函數(shù)引用了循環(huán)控制變量,那么結(jié)果可能會(huì)出乎意料。

本來(lái)以匿名函數(shù)(lambda)來(lái)解釋更清晰,但因?yàn)樯形唇榻B匿名函數(shù),所以這里采用命名函數(shù)為例。

下面的代碼中,將5個(gè)函數(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é)果也全都一樣。換句話說(shuō),保存到列表中的各個(gè)函數(shù)n()中所引用的循環(huán)控制變量"i"并沒(méi)有因?yàn)檠h(huán)的迭代而改變,而且列表中所有函數(shù)保存的i的值都是循環(huán)的最后一個(gè)元素i=4。

(注:對(duì)于此現(xiàn)象,各語(yǔ)言基本都是如此,本節(jié)稍作解釋?zhuān)嬲谋举|(zhì)原因在本文的最后一節(jié)做了額外的補(bǔ)充解釋代碼塊細(xì)述)。

先看下面的例子:

def f1():
 for i in range(5):
 def n():
 print(i)
 return n

f1()()

結(jié)果輸出4。可見(jiàn),print(i)的值并沒(méi)有隨循環(huán)的迭代過(guò)程而改變。

究其原因,是因?yàn)?code>def n()只是函數(shù)的聲明,它不會(huì)去查找i的值是多少,所以不會(huì)將i的值替換到函數(shù)n()的i變量,而是直接保存變量i的地址,當(dāng)循環(huán)結(jié)束時(shí),i指向最后一個(gè)元素i=4的地址。

當(dāng)開(kāi)始調(diào)用n()的時(shí)候,即f1()(),才會(huì)真正開(kāi)始查找i的值,這時(shí)候i指向的正是i=4。

這就像下面的代碼一樣,在還沒(méi)有開(kāi)始調(diào)用f()的時(shí)候,f()內(nèi)部的x一直都只是指向它所看見(jiàn)的變量x,而這個(gè)x是全局作用域范圍。當(dāng)真正開(kāi)始調(diào)用f()的時(shí)候,才會(huì)去定位x的指向。

x=3
def f():
 print(x)

回到上面循環(huán)中的嵌套函數(shù),如果要保證循環(huán)的迭代能作用到其內(nèi)部的函數(shù)中,可以采用默認(rèn)參數(shù)值的方式進(jìn)行賦值:

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è)置默認(rèn)參數(shù)值,等號(hào)右邊的i是函數(shù)聲明時(shí)就查找并替換完成的,所以每次循環(huán)迭代過(guò)程中,等號(hào)右邊的i都不同,等號(hào)左邊的參數(shù)i的默認(rèn)值就不同。

再述作用域規(guī)則

python的作用域是詞法作用域,這意味著函數(shù)的定義位置決定了它所看見(jiàn)的變量。除了詞法作用域,還有動(dòng)態(tài)作用域,動(dòng)態(tài)作用域意味著函數(shù)的調(diào)用位置決定了它所看見(jiàn)的變量。關(guān)于詞法、動(dòng)態(tài)作用域,本文不多做解釋?zhuān)胍私獾脑挘梢詤⒖家晃母愣涸~法作用域、動(dòng)態(tài)作用域、回調(diào)函數(shù)、閉包

下面是本文開(kāi)頭的問(wèn)題:

x=1
def f():
 x=3
 g()
 print("f:",x) # 3

def g():
 print("g:",x) # 1

f()
print("main:",x) # 1

對(duì)于python的這段代碼來(lái)說(shuō),這里有兩個(gè)值得注意的地方:

  • 調(diào)用函數(shù)之前,理論上要先定義好函數(shù),但這里g()的調(diào)用似乎看上去比g()的定義更先
  • f()中調(diào)用g()時(shí),為什么g()輸出的是1而不是3

第一個(gè)問(wèn)題在前文已經(jīng)解釋過(guò)了,再解釋一遍:雖然f()里面有g(shù)()的調(diào)用語(yǔ)句,但def f()只是聲明,但在調(diào)用f()之前,是不會(huì)去調(diào)用g()的。所以,只要f()的調(diào)用語(yǔ)句在def g()之后,就是正確的。

第二個(gè)問(wèn)題,python是詞法作用域,所以:

(1).首先聲明def f(),在此期間會(huì)創(chuàng)建一個(gè)本地變量x,并且print("f:",x)中的x指向這個(gè)本地變量;

(2).然后聲明g(),在此期間,g()的定義語(yǔ)句不在f()內(nèi)部,而是在全局范圍,所以它看見(jiàn)的是x是全局x,所以print("g:",x)中的x指向全局變量x;

當(dāng)調(diào)用f()的時(shí)候,執(zhí)行到g()時(shí),g()中所指向的是全局范圍的x,而非f()段中的x。所以,輸出1。

再看一個(gè)嵌套在函數(shù)內(nèi)部的示例:

x=3

def f1():
 x=4
 def f2():
 print(x)
 x=5
 f2()
f1() # 輸出5

這里的問(wèn)題是f2()中的print為什么不輸出4,而是輸出5。

其實(shí)也很容易理解,因?yàn)?code>def f2()是定義在f1()內(nèi)部的,所以f2()看見(jiàn)的x是f1()中的x,也就是說(shuō)print(x)中的x指向的是f1()中的x。但在調(diào)用f2()之前,重新賦值了x=5,等到調(diào)用f2()的時(shí)候,根據(jù)x的指向,將找到新的x的值。

也就是說(shuō),前面的示例中,有兩個(gè)獨(dú)立的變量x:全局的和f()本地的。后面這個(gè)示例中只有一個(gè)變量x,屬于f()。

代碼塊細(xì)述(必看)

代碼塊可以使得一段python代碼作為一個(gè)單元、一個(gè)整體執(zhí)行。以下是 官方手冊(cè) 的描述。

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.

所以,有以下幾種類(lèi)型的代碼塊:

  1. 模塊文件是一個(gè)代碼塊
  2. 函數(shù)體是一個(gè)代碼塊
  3. class的定義是一個(gè)代碼塊
  4. 交互式(python idle)的每一個(gè)命令行都是一個(gè)獨(dú)立的代碼塊
  5. 腳本文件是一個(gè)代碼塊
  6. 腳本命令是一個(gè)代碼塊(python -c "xxx")
  7. eval()和exec()中的內(nèi)容也都有各自的代碼塊

代碼塊的作用是組織代碼,同時(shí)意味著退出代碼區(qū)塊范圍就退出了作用域范圍。例如退出函數(shù)區(qū)塊,就退出了函數(shù)的作用域,使得函數(shù)內(nèi)的本地變量無(wú)法被函數(shù)的外界訪問(wèn)。

此外,python是解釋性語(yǔ)言,讀一行解釋一行,這意味著每讀一行就忘記前一行。但實(shí)際上更嚴(yán)格的說(shuō)法是讀一個(gè)代碼塊解釋一個(gè)代碼塊,這意味著讀代碼塊中的內(nèi)容時(shí),是暫時(shí)記住屬于這個(gè)代碼塊中所讀內(nèi)容的,讀完整個(gè)代碼塊后再以統(tǒng)籌的形式解釋這個(gè)代碼塊。

先說(shuō)明讀一行解釋一行的情況,也就是每一行都屬于一個(gè)代碼塊,這個(gè)只能通過(guò)python的交互式工具idle工具來(lái)測(cè)試:

>>> x=2000
>>> y=2000
>>> x is y
False
>>> x=2000;y=2000
>>> x is y
True

理論上分號(hào)是語(yǔ)句的分隔符,并不會(huì)影響結(jié)果。但為什么第一個(gè)x is y為False,而第二個(gè)x is y為T(mén)rue?

首先分析第一個(gè)x is y。由于交互式工具idle中每一個(gè)命令都是一個(gè)單獨(dú)的語(yǔ)句塊,這使得解釋完x=2000后立刻就忘記了2000這個(gè)數(shù)值對(duì)象,同時(shí)忘記的還有x變量本身。然后再讀取解釋y=2000,因?yàn)椴挥浀脛偛沤忉尩?code>x=2000,所以會(huì)在內(nèi)存中重新創(chuàng)建一個(gè)數(shù)值結(jié)構(gòu)用來(lái)保存2000這個(gè)數(shù)值,然后用y指向它。換句話說(shuō),x和y所指向的2000在內(nèi)存中是不同的數(shù)據(jù)對(duì)象,所以x is y為False。

下面的x is y返回True:

>>> x=2000;y=2000
>>> x is y
True

因?yàn)閜ython按行解釋?zhuān)粋€(gè)命令是一個(gè)代碼塊。對(duì)于x=2000;y=2000,python首先讀取這一整行,發(fā)現(xiàn)x和y的數(shù)值對(duì)象都是2000,于是做個(gè)簡(jiǎn)單優(yōu)化,等價(jià)于x,y=2000,2000,這意味著它們屬于一個(gè)代碼塊內(nèi),由于都是2000,所以只會(huì)在內(nèi)存中創(chuàng)建一個(gè)數(shù)據(jù)對(duì)象,然后x和y都引用這個(gè)數(shù)據(jù)對(duì)象。所以,x is y返回True。

idle工具中每個(gè)命令都是獨(dú)立的代碼塊,但是py文件卻是一個(gè)完整的代碼塊,其內(nèi)還可以嵌套其它代碼塊(如函數(shù)、exec()等)。所以,如果上面的分行賦值語(yǔ)句放在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)建一個(gè)屬于全局作用域的2000數(shù)據(jù)對(duì)象,再解釋y=2000的時(shí)候,發(fā)現(xiàn)這個(gè)全局對(duì)象2000已經(jīng)存在了(因?yàn)閤和y同處于全局代碼塊內(nèi)),所以不會(huì)再額外創(chuàng)建新的2000對(duì)象。這里反映出來(lái)的結(jié)果是"同一個(gè)代碼塊內(nèi),雖然仍然是讀一行解釋一行,但在退出這個(gè)代碼塊之前,不會(huì)忘記這個(gè)代碼塊中的內(nèi)容,而且會(huì)統(tǒng)籌安排這個(gè)代碼塊"。

同理def f1()內(nèi)的代碼塊,因?yàn)閦是本地作用域的變量,更標(biāo)準(zhǔn)的是處于不同代碼塊內(nèi),所以會(huì)在本地作用域內(nèi)存區(qū)創(chuàng)建新的數(shù)據(jù)對(duì)象2000,所以x is z返回False。根據(jù)前面的解釋?zhuān)?code>z1 is z返回True。

再回顧前文多次出現(xiàn)的一個(gè)異常:

x = 3
def f1():
 print(x)
 x=4
f1()

報(bào)錯(cuò)信息:

UnboundLocalError: local variable 'x' referenced before assignment

當(dāng)執(zhí)行到def語(yǔ)句的時(shí)候,因?yàn)閐ef聲明函數(shù),函數(shù)體是一個(gè)代碼塊,所以按照代碼塊的方式讀取屬于這個(gè)代碼塊中的內(nèi)容。首先讀取print(x),但并不會(huì)直接解釋?zhuān)菚?huì)記住它,并繼續(xù)向下讀取,于是讀取x=4,這意味著x是一個(gè)本地變量。然后統(tǒng)籌安排整個(gè)代碼塊,將print(x)的x認(rèn)為是本地變量而非全局變量。注意,直到def退出的時(shí)候都還沒(méi)有進(jìn)行x的賦值,而是記錄了本地變量x,賦值操作是在函數(shù)調(diào)用的時(shí)候進(jìn)行的。當(dāng)調(diào)用函數(shù)f()的時(shí)候,發(fā)現(xiàn)print(x)中的x是本地變量,但因?yàn)檫€沒(méi)有賦值,所以報(bào)錯(cuò)。

但是再看下面的,為什么又返回True?

>>> x=256
>>> y=256
>>> x is y
True

因?yàn)镻ython在啟動(dòng)的時(shí)候就在內(nèi)存中預(yù)先為常用的較小整數(shù)值(-5到256)創(chuàng)建好了對(duì)象,因?yàn)樗鼈兪褂玫姆浅nl繁(有些在python的內(nèi)部已經(jīng)使用了)。所以,對(duì)于這個(gè)范圍內(nèi)的整數(shù),都是直接引用,不會(huì)再在內(nèi)存中額外創(chuàng)建新的數(shù)值對(duì)象,所以x is y總是返回true。甚至,這些小值整數(shù)可以跨作用域:

x = 3 
def f1():
 y=3
 print(x is y) # True

f1()

再看前文循環(huán)內(nèi)的函數(shù)的問(wèn)題。

def f1():
 for i in range(5):
 def n():
 print(i)
 return n

f1()()

前面對(duì)現(xiàn)象已經(jīng)解釋過(guò),內(nèi)部函數(shù)n()中print(i)的i不會(huì)隨循環(huán)的迭代而改變,而是固定的值i=4。

python首先解釋def f1()的代碼塊,會(huì)記錄屬于這個(gè)代碼塊作用域內(nèi)的變量i和n,但i和n都不會(huì)賦值,也就是說(shuō)暫時(shí)并不知道變量n是一個(gè)函數(shù)變量。

同理,當(dāng)需要解釋def n()代碼塊的時(shí)候,將記住這個(gè)代碼塊涉及到的變量i,只不過(guò)這個(gè)變量i是屬于外層函數(shù)的,但不管如何,這個(gè)代碼塊記住了i,且記住了它是外部函數(shù)作用域的。

注意,函數(shù)的聲明過(guò)程中,所有涉及到變量i的作用域內(nèi)都不會(huì)對(duì)i進(jìn)行賦值,僅僅只是保存了這個(gè)i變量名,只有在調(diào)用函數(shù)的時(shí)候才會(huì)進(jìn)行賦值操作。

當(dāng)開(kāi)始調(diào)用f1()的時(shí)候,開(kāi)始執(zhí)行函數(shù)體中的代碼,于是開(kāi)始循環(huán)迭代,且多次聲明函數(shù)n(),每一次迭代生成的n()都會(huì)讓原先已記錄的變量n指向這個(gè)新聲明的函數(shù)體(相當(dāng)于賦值的操作,只不過(guò)是變量n引用的對(duì)象是函數(shù)體結(jié)構(gòu),而不是一般的數(shù)據(jù)對(duì)象),由于只是在循環(huán)中聲明函數(shù)n(),并沒(méi)有進(jìn)行調(diào)用,所以不會(huì)對(duì)n()中的i進(jìn)行賦值操作。而且,每次循環(huán)迭代都會(huì)讓變量n指向新的函數(shù)體,使得先前迭代過(guò)程中定義的函數(shù)被丟棄(覆蓋),所以最終只記住了最后一輪循環(huán)時(shí)聲明的函數(shù)n(),并且i=4。

當(dāng)調(diào)用f1()()時(shí),表示調(diào)用f1()中返回的函數(shù)n(),直到這個(gè)時(shí)候才會(huì)對(duì)n()內(nèi)的i進(jìn)行賦值,賦值時(shí)將搜索它的外層函數(shù)f1()作用域,發(fā)現(xiàn)這個(gè)作用域內(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()的時(shí)候,執(zhí)行循環(huán)的迭代,每次迭代時(shí)都會(huì)調(diào)用n(),意味著每次迭代都要對(duì)n()中的i進(jìn)行賦值。

另外注意,前面說(shuō)過(guò),函數(shù)的默認(rèn)參數(shù)是在函數(shù)聲明時(shí)進(jìn)行賦值的,所以下面的列表L中每個(gè)元素所代表的函數(shù),它們的變量i都指向不同的數(shù)值對(duì)象。

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é)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)億速云的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI