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: