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

Tags: