Python UnboundLocalError和NameError錯誤根源解析

alpha_panda發表於2018-10-31

如果程式碼風格相對而言不是那麼的pythonic,或許很少碰到這類錯誤。當然並不是不鼓勵使用一些python語言的技巧。如果遇到這這種型別的錯誤,說明我們對python中變數引用相關部分有不當的認識和理解。而這又是對理解python相關概念比較重要的。這也是本文寫作的原因。

 本文為理解閉包相關概念的做鋪墊,後續會詳細深入的整理出閉包相關的博文,敬請關注。

1.案例分析

在整理閉包相關概念的過程中,經常發現UnboundLocalError和NameError這兩個錯誤,剛開始遇到的時候可能很困惑,對這樣的錯誤無從下手。

1.1 案例一:

1 def outer_func():
2     loc_var = "local variable"
3     def inner_func():
4         loc_var += " in inner func"
5         return loc_var
6     return inner_func
7 
8 clo_func = outer_func()
9 clo_func()

錯誤提示:

Traceback (most recent call last):
  File "G:Project FilesPython TestMain.py", line 238, in <module>
    clo_func()
  File "G:Project FilesPython TestMain.py", line 233, in inner_func
    loc_var += " in inner func"
UnboundLocalError: local variable `loc_var` referenced before assignment

1.2 案例二:

1 def get_select_desc(name, flag, is_format = True):
2     if flag:
3         sel_res = `Do select name = %s` % name
4     return sel_res if is_format else name
5 
6 get_select_desc(`Error`, False, True)

錯誤提示:

Traceback (most recent call last):
  File "G:Project FilesPython TestMain.py", line 247, in <module>
    get_select_desc(`Error`, False, True)
  File "G:Project FilesPython TestMain.py", line 245, in get_select_desc
    return sel_res if is_format else name
UnboundLocalError: local variable `sel_res` referenced before assignment

1.3 案例三:

 1 def outer_func(out_flag):
 2     if out_flag:
 3         loc_var1 = `local variable with flag`
 4     else:
 5         loc_var2 = `local variable without flag`
 6     def inner_func(in_flag):
 7         return loc_var1 if in_flag else loc_var2
 8     return inner_func
 9 
10 clo_func = outer_func(True)
11 print clo_func(False)

  錯誤提示:

Traceback (most recent call last):
  File "G:Project FilesPython TestMain.py", line 260, in <module>
    print clo_func(False)
  File "G:Project FilesPython TestMain.py", line 256, in inner_func
    return loc_var1 if in_flag else loc_var2
NameError: free variable `loc_var2` referenced before assignment in enclosing scope

 上面的三個例子可能顯得有點矯揉造作,但是實際上類似錯誤的程式碼都或多或少可以在上面的例子中找到影子。這裡僅僅為了說明相關概念,對例子本身的合理性不必做過多的關注。

2.錯誤原因

由於python中沒有變數、函式或者類的宣告概念。按照C或者C++的習慣編寫python,或許很難發現錯誤的根源在哪。

首先看一下這類錯誤的官方解釋:

When a name is not found at all, a NameError exception is raised. If the name refers to a local variable that has not been bound, a UnboundLocalError exception is raised. UnboundLocalError is a subclass of NameError.

大概意思是:

如果引用了某個變數,但是變數名沒有找到,該型別的錯誤就是NameError。如果該名字是一個還沒有被繫結的區域性變數名,那麼該型別的錯誤是NameError中的UnboundLocalError錯誤

下面的這種NameError型別的錯誤或許還好理解一些:

1 my_function()
2 def my_function():
3     pass

 如果說python直譯器執行到def my_function()時才繫結到my_function,而my_function此時也表示的是記憶體中函式執行的入口。因此在此之前使用my_function均會有NameError錯誤。

那麼上面的例子中使用變數前,都有賦值操作(可視為一種繫結操作,後面會講),為什麼引用時會出錯?定義也可判斷可見性

如果說是因為賦值操作沒有執行,那麼為什麼該變數名在區域性名稱空間是可見的?(不可見的話,會有這類錯誤:NameError: global name `xxx` is not defined,根據UnboundLocalError定義也可判斷可見性)

問題到底出在哪裡?怎樣正確理解上面三個例子中的錯誤?

3. 可見性與繫結 

簡單起見,這裡不介紹名稱空間與變數查詢規則LGB相關的概念。

在C或者C++中,只要宣告並定義了一個變數或者函式,便可以直接使用。但是在Python中要想引用一個name,該name必須要可見而且是繫結的。

先了解一下幾個概念:

  1. code block:作為一個單元(Unit)被執行的一段python程式文字。例如一個模組、函式體和類的定義等。
  2. scope:在一個code block中定義name的可見性;
  3. block’s environment:對於一個code block,其所有scope中可見的name的集合構成block的環境。
  4. bind name:下面的操作均可視為繫結操作
    • 函式的形參
    • import宣告
    • 類和函式的定義
    • 賦值操作
    • for迴圈首標
    • 異常捕獲中相關的賦值變數
  5. local variable:如果name在一個block中被繫結,該變數便是該block的一個local variable。
  6. global variable:如果name在一個module中被繫結,該變數便稱為一個global variable。
  7. free variable: 如果一個name在一個block中被引用,但沒有在該程式碼塊中被定義,那麼便稱為該變數為一個free variable。

Free variable是一個比較重要的概念,在閉包中引用的父函式中的區域性變數是一個free variable,而且該free variable被存放在一個cell物件中。這個會在閉包相關的文章中介紹。 

scope在函式中具有可擴充套件性,但在類定義中不具有可擴充套件性。

分析整理一下:

經過上面的一些概念介紹我們知道了,一個變數只要在其code block中有繫結操作,那麼在code block的scope中便包含有這個變數。

也就是繫結操作決定了,被繫結的name在當前scope(如果是函式的話,也包括其中定義的scope)中是可見的,哪怕是在name進行真正的繫結操作之前。

這裡就會有一個問題,那就是如果在繫結name操作之前引用了該name,那麼就會出現問題,即使該name是可見的。

If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.

注意上面官方描述的第一句和最後一句話。

總的來說就是在一個code block中,所有繫結操作中被繫結的name均可以視為一個local variable;但是直到繫結操作被執行之後才可以真正的引用該name。

有了這些概念,下面逐一分析一下上面的三個案例。

4. 錯誤解析

4.1 案例一分析

在outer_func中我們定義了變數loc_var,因為賦值是一種繫結操作,因此loc_var具有可見性,並且被繫結到了具體的字串物件。

但是在其中定義的函式inner_func中卻並不能引用,函式中的scope不是可以擴充套件到其內定義的所有scope中嗎?

下面在在來看一下官方的兩段文字描述:

When a name is used in a code block, it is resolved using the nearest enclosing scope.

這段話告訴我們當一個name被引用時,他會在其最近的scope中尋找被引用name的定義。顯然loc_var += ” in inner func”這個語句中的loc_var會先在內部函式inner_func中找尋name loc_var。

該語句實際上等價於loc_var = loc_var + ” in inner func”,等號右邊的loc_var變數會首先被使用,但這裡並不會使用outer_func中定義的loc_var,因為在函式inner_func的scope中有loc_var的賦值操作,因此這個變數在inner_func的scope中作為inner_func的一個local variable是可見的。

但是要等該語句執行完成,才能真正繫結loc_var。也就是此語句中我們使用了inner_func block中的被繫結之前的一個local variable。根據上面錯誤型別的定義,這是一個UnboundLocalError.

4.2 案例二分析

在這個例子中,看上去好像有問題,但是又不知道怎麼解釋。

引用發生在繫結操作之後,該變數應該可以被正常引用。但問題就在於賦值語句(繫結操作)不一定被執行。如果沒有繫結操作那麼對變數的引用肯定會有問題,這個前面已經解釋過了。

但是還有一個疑問可能在於,如果賦值語句沒有被執行,那麼變數在當前block中為什麼是可見的?

關於這個問題其實可以被上面的一段話解釋:The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.

只要有繫結操作(不管實際有沒有被執行),那麼被繫結的name可以作為一個local variable,也就是在當前block中是可見的。scanning text發生在程式碼被執行前。

4.2 案例三分析

這個例子主要說明了一類對free variable引用的問題。同時這個例子也展示了一個free variable的使用。

在建立閉包inner_func時,loc_var1和loc_var2作為父函式outer_func中的兩個local variable在其內部inner_func的scope中是可見的。返回閉包之後在閉包中被引用outer_func中的local variable將作為稱為一個free variable.

閉包中的free variable可不可以被引用取決於它們有沒有被繫結到具體的物件。

5. 引申案例

下面再來看一個例子:

1 import sys
2 
3 def add_path(new_path):
4     path_list = sys.path
5 
6     if new_path not in path_list:
7         import sys
8         sys.path.append(new_path)
9 add_path(`./`)

平時不經意間可能就會犯上面的這個錯誤,這也是一個典型的UnboundLocalError錯誤。如果仔細的閱讀並理解上面的分析過程,相信應給能夠理解這個錯誤的原因。如果還不太清除,請再閱讀一遍 :-)

如果有什麼疑問歡迎討論。

相關文章