Python UnboundLocalError和NameError錯誤根源解析

若是代碼風格相對而言不是那麼的pythonic,或許不多碰到這類錯誤。固然並非不鼓勵使用一些python語言的技巧。若是遇到這這種類型的錯誤,說明咱們對python中變量引用相關部分有不當的認識和理解。而這又是對理解python相關概念比較重要的。這也是本文寫做的緣由。python

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

1.案例分析

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

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 Files\Python Test\Main.py", line 238, in <module>
    clo_func()
  File "G:\Project Files\Python Test\Main.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)

錯誤提示:spa

Traceback (most recent call last):
  File "G:\Project Files\Python Test\Main.py", line 247, in <module>
    get_select_desc('Error', False, True)
  File "G:\Project Files\Python Test\Main.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)

  錯誤提示:rest

Traceback (most recent call last):
  File "G:\Project Files\Python Test\Main.py", line 260, in <module>
    print clo_func(False)
  File "G:\Project Files\Python Test\Main.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

 上面的三個例子可能顯得有點矯揉造做,可是實際上相似錯誤的代碼都或多或少能夠在上面的例子中找到影子。這裏僅僅爲了說明相關概念,對例子自己的合理性沒必要作過多的關注。code

2.錯誤緣由

因爲python中沒有變量、函數或者類的聲明概念。按照C或者C++的習慣編寫python,或許很難發現錯誤的根源在哪。orm

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

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.blog

大概意思是:

若是引用了某個變量,可是變量名沒有找到,該類型的錯誤就是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錯誤。若是仔細的閱讀並理解上面的分析過程,相信應給可以理解這個錯誤的緣由。若是還不太清除,請再閱讀一遍 :-)

若是有什麼疑問歡迎討論。

相關文章
相關標籤/搜索