Python作用域
在Python程序中使用變量的時候,都是在所謂的命名空間中進行,命名空間也是存放變量的地方,變量的作用域就是指的命名空間。變量在代碼中被賦值時的位置(嵌套深度)就決定了該變量的命名空間(即該變量能被訪問到的範圍)。Python將一個變量名被賦值的地點關聯爲一個命名空間。
函數爲Python程序增加了一個額外的命名空間層,即在默認情況下,一個在函數內部聲明的變量,只能在函數內部被使用:
- 一個在def函數內部被聲明的變量,在默認情況下,只能在函數內部使用。
- def之中的命名空間與def之外的命名空間是不重複的,即使有相同的變量名,它們之中的變量也不會衝突。
作用域法則
- 內嵌的模塊是全局作用域,創建於頂層的變量,都是在該模塊中的全局作用域中。對於一個外部的模塊,需要像使用對象屬性一樣,使用外部模塊的變量。也可以說是兩個模塊的命名空間是不一樣的,沒有包含關係,是並列的。在使用時,變量名要由模塊文件來區分命名空間(如sys.path),要精確指出使用的模塊,才能使用該模塊中的變量。
- 全局作用域的作用範圍僅限於單個文件。這裏所說的“全局”是指單個模塊文件中的頂層變量是對於該文件內部的代碼而言是全局的。
- 每次對函數的調用都創建了一個新的本地作用域。函數的創建定義了一個函數作用域,而函數的調用會創建一個新的本地作用域,創建的函數可能會被調用多次,每調用一次都會創建一個新的本地作用域。
- 賦值的變量名除非聲明爲全局變量(global)或非本地變量(nonlocal),否則默認全部爲本地變量。在沒有任何作用域聲明語句使用的情況下,函數內部聲明的變量全部爲函數本地變量。
- 函數內部的變量名查找會按照順序查找作用域:本地變量->全局變量->內置變量
需要注意的是,在原處修改對象並不會改變變量的作用域,實際上只有對變量名賦值纔可以。
變量名解析:LEGB原則
作用域可以總結爲以下三點:
- 變量名的引用分爲以下三個作用域,按照先後順序依次查找:本地->函數內(如果嵌套的函數)->全局->內置。
- 默認情況下,變量名賦值創建或改變的是本地的變量。
- 全局聲明(global)和非本地(nonlocal)聲明將賦值的變量名映射到模塊文件內部的作用域。
內置作用域
內置作用域是定義在一個內置的模塊中,名爲builtins,在Python內部,查找變量名時會自動搜索這個模塊中的變量,因此我們不需要在程序中明文導入該模塊。由於LEGB作用域查找法則,先查找的作用域內部的變量,在變量名相同的情況下,很有可能覆蓋存在於後續要查找的作用域內的變量。
global語句
global是聲明一個全局變量的聲明語句。全局變量有以下特徵:
- 全局變量是位於模塊文件內部的頂層的變量名
- 全局變量如果要在函數內部被賦值的話,就必須要聲明(global語句)
- 全局變量名在函數的內部不經過聲明也可以被引用
global語句包含global關鍵字,後面跟着一個或多個變量名,這些變量名被聲明之後,它們就存在於文件模塊的全局作用域中。
>>> x=123
>>> y=456
>>> def func():
... global z #在函數內部將z聲明爲全局變量
... z=999
...
>>> func()
>>> x+y+z #這樣z即可在文件模塊中使用,z已經成爲全局變量
1578
>>> def func():
... global z=3 #使用global時,不能將它與賦值語句一起使用,要先聲明,再使用
File "<stdin>", line 2
global z=3
^
SyntaxError: invalid syntax
>>>
全局變量應該較少使用,在函數中多使用本地變量,降低函數的耦合性,全局變量讓程序變得更不容易理解,函數存在的意義是爲了完成某項功能與任務,如果全局變量用得過多,這種函數會對模塊造成不小影響,所以應避免過多在函數中聲明全局變量。
全局變量有時是爲了在退出函數時,保存某些信息,在下次調用時使用。全局變量在並行線程中在不同函數之間是一塊共享的內存,所以有些時候全局變量充當了在不同函數之間通信的工具。
嵌套函數
在一個函數f1中嵌套着另外一個函數f2對象,而且該函數對象f2只能被嵌套的函數f1使用,f2是一個臨時函數,只在f1函數執行過程中存在,並且只對f1可見:
>>> def f1(x):
... print(x)
... def f2(x): #f2函數嵌套在f1函數中
... print(x+1)
... f2(x)
...
>>> f2(2) #f2函數只在f1函數中可見
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'f2' is not defined
>>> f1(2) #調用f1會調用f2函數
2
3 #調用f2後打印出的值
>>>
工廠函數
工廠函數:一個可以記住嵌套作用域的變量值的函數。
>>> def maker(N):
... def func(x):
... return x**N
... return func #返回一個內嵌的函數對象
...
>>> f=maker(3) #內嵌函數記住了整數3,即被嵌套的函數內部的變量N,儘管這時只是返回的內嵌函數,maker函數已經退出
>>> f(3) #計算3^3,函數對象f記住了內嵌函數中使用的變量3,
27
>>>
外層的maker函數就像是工廠函數一樣,用最初傳遞進去的值,創建了一個函數並返回。
嵌套作用域內的存儲的變量,是在嵌套函數被調用的時候纔開始去查找特定作用域中的變量。
>>> def f1():
... act=[]
... for i in range(5):
... act.append(lambda x: x+i)
... return act
...
>>> l=f1()
>>> l[0](1) #按照嵌套的函數內的邏輯,列表中的函數應該是不一樣的基數
5 #但是嵌套函數在調用的時候纔去作用域中查找變量,所以列表中的函數都會返回相同的值
>>> l[1](1)
5
>>> l[2](1)
5
>>> l[3](1)
5
>>> l[4](1)
5
>>>
如果要達到構建不同函數的效果,就要讓變量傳遞到內嵌函數中,成爲內嵌函數中的變量,這樣列表中的函數就可以在各自的內嵌函數作用域中去查找各自的變量。
>>> def f1():
... act=[]
... for i in range(5):
... act.append(lambda x,i=i: x+i) #將參數傳入內嵌函數中,使得內嵌函數的作用域中有各自不同的變量
... return act
...
>>> l=f1()
>>> l[0](1)
1
>>> l[1](1)
2
>>> l[2](1)
3
>>> l[3](1)
4
>>> l[4](1)
5
>>>
nonlocal語句
nonlocal語句是global的近親,nonlocal語句只是修改嵌套函數作用域內的變量,而不是本地變量也不是全局變量。
語法結構:
>>> def func():
... x=12
... def func2():
... nonlocal x #這裏聲明瞭變量x是函數func()中的x
... x=13 #並把x的值改爲13
... func2()
... print(x) #這裏會輸出13
...
>>> func()
13
>>> x=14
>>> x
14
>>> func()
13
>>> x #並沒有修改全局中的變量
14
>>> def func2():
... nonlocal x #如果聲明nonlocal時,該函數外層並沒有嵌套函數,會報錯
... x=3333
... print(x)
...
File "<stdin>", line 2
SyntaxError: no binding for nonlocal 'x' found
>>>
當運行到nonlocal語句時,nonlocal聲明的變量必須在一個嵌套的def函數內提取被定義過。 global聲明意味着聲明的變量已經在全局變量中被定義過。nonlocal聲明意味着聲明的變量在嵌套的函數中被定義過(只能出現在嵌套的函數中,即使是全局變量也不行)。在一般沒有nonlocal聲明的情況下,Python是不允許修改嵌套函數中的變量的。這種情況下就要使用nonlocal語句
>>> def func():
... x=3
... def func2():
... print(x)
... x=4 #不允許直接修改,只允許訪問。需要修改時,使用nonlocal語句
... print(x)
... func2()
... print(x)
...
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in func
File "<stdin>", line 4, in func2
UnboundLocalError: local variable 'x' referenced before assignment
>>>
>>> def func():
... x=3
... def func2():
... nonlocal x #聲明爲外層func()函數中的x變量
... print(x)
... x=4
... print(x)
... func2()
... print(x)
...
>>> func()
3
4
4
>>>
與global語句類似,nonlocal在一些狀態下也是用來記錄函數調用退出後的一些信息。
小結
全局,非本地,類和函數都提供了保持函數狀態的選項。在默認情況下,變量的作用域取決於變量被賦值的位置,還可以使用global,nonlocal語句來讓變量的作用域更具有多樣性。需要保存某個變量的狀態時,就要考慮到將其所在的命名空間也保存下來,工廠函數就是這樣的道理,當將一個工廠函數賦值給一個變量名(更準確的說是函數名)時,這個函數名就保存着相應的命名空間,相應的也能記錄下命名空間下的內嵌函數和變量名。