07.python函數作用域global、nonlocal、LEGB
函數作用域
作用域
一個標識符的課件範圍,這就是標識符的作用域,一般常說的是變量的作用域
def foo():
x = 100
print(x) # 可以訪問到嗎
上例中x不可以訪問到,會拋出異常(NameError: name ‘x’ is not defined),原因在於函數是一個封裝,它會開闢一個作用域,x變量被限制在這個作用域中,所以在函數外部x變量不可見。
注意:每一個函數都會開闢一個作用域
作用域分類
-
全局作用域
-
在整個程序運行環境中都可見
-
全局作用域中的變量稱為全局變量global
-
-
局部作用域
-
在函數、類等內部可見
-
局部作用域中的變量稱為局部變量,其使用範圍不能超過其所在局部作用域
-
也稱為本地作用域local
-
# 局部變量
def fn1():
x = 1 # 局部作用域,x為局部變量,使用範圍在fn1內
def fn2():
print(x) # x能打印嗎?可見嗎?為什麼?
print(x) # x能打印嗎?可見嗎?為什麼?
# 全局變量
x = 5 # 全局變量,也在函數外定義
def foo():
print(x) # 可見嗎?為什麼?
foo()
-
一般來講外部作用域變量可以在函數內部可見,可以使用
-
反過來,函數內部的局部變量,不能在函數外部看到
函數嵌套
在一個函數中定義了另外一個函數
def outer():
def inner():
print("inner")
inner()
print("outer")
outer() # 可以嗎?
inner() # 可以嗎?
內部函數inner不能在外部直接使用,會拋NameError異常,因為它在函數外部不可見。
其實,inner不過就是一個標識符,就是一個函數outer內部定義的變量而已。
嵌套結構的作用域
對比下面嵌套結構,代碼執行的效果
def outer1():
o = 65
def inner():
print('inner', o, chr(o))
inner()
print('outer', o, chr(o))
outer1() # 執行後,打印什麼
def outer2():
o = 65
def inner():
o = 97
print('inner', o, chr(o))
inner()
print('outer', o, chr(o))
outer2() # 執行後,打印什麼
從執行的結果來看:
-
外層變量在內部作用域可見
-
內層作用域inner中,如果定義了 o = 97 ,相當於在當前函數inner作用域中重新定義了一個新的
-
變量o,但是,這個o並不能覆蓋掉外部作用域outer2中的變量。只不過對於inner函數來說,其只能可見自己作用域中定義的變量o了
內建函數 | 函數簽名 | 說明 |
---|---|---|
chr | chr(i) | 通過unicode編碼返回對應字符 |
ord | ord(c) | 獲得字符對應的unicode |
print(ord('中'), hex(ord('中')), '中'.encode(),'中'.encode('gbk'))
chr(20013) # '中'
chr(97)
一個賦值語句的問題
再看下面左右2個函數
左邊函數 | 右邊函數 |
---|---|
正常執行,函數外部的變量在函數內部可見 | 執行錯誤嗎,為什麼?難道函數內部又不可見了?y = x + 1可以正確執行,可是為什麼x += 1卻不能正確執行? |
仔細觀察函數2返回的錯誤指向x += 1,原因是什麼呢?
x = 5
def foo():
x += 1
foo() # 報錯如下
原因分析:
-
x += 1 其實是 x = x + 1
-
只要有”x=”出現,這就是賦值語句。相當於在foo內部定義一個局部變量x,那麼foo內部所有x都是這個局部變量x了
-
x = x + 1 相當於使用了局部變量x,但是這個x還沒有完成賦值,就被右邊拿來做加1操作了
x = 5
def foo(): # 函數被解釋器解釋,foo指向函數對象,同時解釋器會理解x是什麼作用域
print(x) # x 在函數解析時就被解釋器判定為局部變量
x += 1 # x = x + 1
foo() # 調用時
如何解決這個常見問題?
global語句
x = 5
def foo():
global x # 全局變量
x += 1
print(x)
foo()
-
使用global關鍵字的變量,將foo內的x聲明為使用外部的全局作用域中定義的x
-
全局作用域中必須有x的定義
如果全局作用域中沒有x定義會怎樣?
注意,下面試驗如果在ipython、jupyter中做,上下文運行環境中有可能有x的定義,稍微不注意,就測試不出效果
# 有錯嗎?
def foo():
global x
x += 1
print(x)
foo()
# 有錯嗎?
def foo():
global x
x = 10
x += 1
print(x)
foo()
print(x) # 可以嗎
使用global關鍵字定義的變量,雖然在foo函數中聲明,但是這將告訴當前foo函數作用域,這個x變量將使用外部全局作用域中的x。
即使是在foo中又寫了 x = 10 ,也不會在foo這個局部作用域中定義局部變量x了。
使用了global,foo中的x不再是局部變量了,它是全局變量。
總結
-
x+=1 這種是特殊形式產生的錯誤的原因?先引用後賦值,而python動態語言是賦值才算定義,才能被引用。解決辦法,在這條語句前增加x=0之類的賦值語句,或者使用global 告訴內部作用域,去全局作用域查找變量定義
-
內部作用域使用 x = 10 之類的賦值語句會重新定義局部作用域使用的變量x,但是,一旦這個作用域中使用 global 聲明x為全局的,那麼x=5相當於在為全局作用域的變量x賦值
global使用原則
-
外部作用域變量會在內部作用域可見,但也不要在這個內部的局部作用域中直接使用,因為函數的目的就是為了封裝,盡量與外界隔離
-
如果函數需要使用外部全局變量,請盡量使用函數的形參定義,並在調用傳實參解決
-
一句話:不用global。學習它就是為了深入理解變量作用域
閉包
自由變量:未在本地作用域中定義的變量。例如定義在內層函數外的外層函數的作用域中的變量
閉包:就是一個概念,出現在嵌套函數中,指的是內層函數引用到了外層函數的自由變量,就形成了閉包。很多語言都有這個概念,最熟悉就是JavaScript
def counter():
c = [0]
def inc():
c[0] += 1 # 報錯嗎? 為什麼 # line 4
return c[0]
return inc
foo = counter() # line 8
print(foo(), foo()) # line 9
c = 100
print(foo()) # line 11
上面代碼有幾個問題:
-
第4行會報錯嗎?為什麼
-
第9行打印什麼結果?
-
第11行打印什麼結果?
代碼分析
-
第8行會執行counter函數並返回inc對應的函數對象,注意這個函數對象並不釋放,因為有foo記着
-
第4行會報錯嗎?為什麼
- 不會報錯,c已經在counter函數中定義過了。而且inc中的使用方式是為c的元素修改值,而不是重新定義c變量
-
第9行打印什麼結果?
- 打印 1 2
-
第11行打印什麼結果?
-
打印 3
-
第9行的c和counter中的c不一樣,而inc引用的是自由變量正是counter中的變量c
-
這是Python2中實現閉包的方式,Python3還可以使用nonlocal關鍵字
再看下面這段代碼,會報錯嗎?使用global能解決嗎?
def counter():
count = 0
def inc():
count += 1
return count
return inc
foo = counter()
print(foo(), foo())
上例一定出錯,使用gobal可以解決
def counter():
global count
count = 0
def inc():
global count
count += 1
return count
return inc
foo = counter()
print(foo(), foo())
count = 100
print(foo()) # 打印幾?
上例使用global解決,這是全局變量的實現,而不是閉包了。
如果要對這個普通變量使用閉包,Python3中可以使用nonlocal關鍵字。
nonlocal語句
nonlocal:將變量標記為不在本地作用域定義,而是在上級的某一級局部作用域中定義,但不能是全局作用域中定義。
def counter():
count = 0
def inc():
nonlocal count # 聲明變量count不是本地變量
count += 1
return count
return inc
foo = counter()
print(foo(), foo())
count 是外層函數的局部變量,被內部函數引用。
內部函數使用nonlocal關鍵字聲明count變量在上級作用域而非本地作用域中定義。
代碼中內層函數引用外部局部作用域中的自由變量,形成閉包。
上例是錯誤的,nonlocal 聲明變量 a 不在當前作用域,但是往外就是全局作用域了,所以錯誤。
函數的銷毀
定義一個函數就是生成一個函數對象,函數名指向的就是函數對象。
可以使用del語句刪除函數,使其引用計數減1。
可以使用同名標識符覆蓋原有定義,本質上也是使其引用計數減1。
Python程序結束時,所有對象銷毀。
函數也是對象,也不例外,是否銷毀,還是看引用計數是否減為0。
變量名解析原則LEGB
-
Local,本地作用域、局部作用域的local命名空間。函數調用時創建,調用結束消亡
-
Enclosing,Python2.2時引入了嵌套函數,實現了閉包,這個就是嵌套函數的外部函數的命名空間
-
Global,全局作用域,即一個模塊的命名空間。模塊被import時創建,解釋器退出時消亡
-
Build-in,內置模塊的命名空間,生命周期從python解釋器啟動時創建到解釋器退出時消亡。例如print(open),print和open都是內置的變量
所以一個名詞的查找順序就是LEGB