1. 程式人生 > 其它 >07.python函式作用域global、nonlocal、LEGB

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

作者:宋城西柵 出處:http://www.cnblogs.com/shwee/

-------------------------------------------

個性簽名:獨學而無友,則孤陋而寡聞。做一個靈魂有趣的人!

如果覺得這篇文章對你有小小的幫助的話,記得在右下角點個“推薦”哦,博主在此感謝!

萬水千山總是情,打賞一分行不行,所以如果你心情還比較高興,也是可以掃碼打賞博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!