2017-02-25 184 views
7
def fun(): 
    if False: 
     x=3 
    print(locals()) 
    print(x) 
fun() 

輸出和錯誤消息:Python的局部變量編譯原理

{} 
--------------------------------------------------------------------------- 
UnboundLocalError       Traceback (most recent call last) 
<ipython-input-57-d9deb3063ae1> in <module>() 
     4  print(locals()) 
     5  print(x) 
----> 6 fun() 

<ipython-input-57-d9deb3063ae1> in fun() 
     3   x=3 
     4  print(locals()) 
----> 5  print(x) 
     6 fun() 

UnboundLocalError: local variable 'x' referenced before assignment 

我想知道Python解釋器是如何工作的。請注意,x = 3根本不運行,並且不應將其視爲局部變量,這意味着錯誤將爲「name'x'未定義」。但看看代碼和錯誤消息,情況並非如此。有人能解釋這種情況下編譯python解釋器的機制原理嗎?

+0

可能的重複http://stackoverflow.com/q/7969949/3758972 –

+0

這可能是相關的:[範圍規則簡述](http://stackoverflow.com/questions/291978/short-description-of-範圍規則) –

+0

如果下面的答案之一解決了您的問題,您應該接受它(單擊相應答案旁邊的複選標記)。這有兩件事。它讓每個人都知道你的問題已經得到解決,讓你滿意,並且它可以幫助你幫助你。請參閱[此處](http://meta.stackexchange.com/a/5235)以獲取完整說明。 –

回答

4

所以,Python會始終處於各功能分類,每一個名字爲當地的之一,非本地全球。這些名稱範圍是排他性的;在每個函數中(嵌套函數中的名稱都有自己的命名範圍),每個名稱只能屬於這些類別中的一個。

當Python編譯的代碼:

def fun(): 
    if False: 
     x=3 

它將導致抽象語法樹中:

FunctionDef(
    name='fun', 
    args=arguments(...), b 
    body=[ 
     If(test=NameConstant(value=False), 
      body=[ 
       Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=3)) 
      ], 
      orelse=[]) 
    ] 
) 

(一些東西爲簡潔起見省略)。現在,當這個抽象語法樹被編譯成代碼時,Python將掃描所有名稱節點。如果有任何Name節點,與ctx=Store(),那名被認爲是當地給封閉FunctionDef如果有的話,除非global(即global x)或nonlocalnonlocal x)語句相同功能的定義中覆蓋。

ctx=Store()將主要發生在作業左側使用相關名稱或for循環中的迭代變量時。

現在,當Python的編譯此字節碼,生成的字節碼是

>>> dis.dis(fun) 
    4   0 LOAD_GLOBAL    0 (print) 
       3 LOAD_FAST    0 (x) 
       6 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
       9 POP_TOP 
      10 LOAD_CONST    0 (None) 
      13 RETURN_VALUE 

優化完全清除if聲明;但由於該變量已在本地標記爲該函數,所以LOAD_FAST用於x,這將導致x本地變量和僅局部變量被訪問。由於x尚未設置,因此拋出了UnboundLocalError。另一方面,名稱print從未被分配到,因此被認爲是該函數內的全局名稱,因此其值被加載到LOAD_GLOBAL

3

x = 3無法訪問的事實是無關緊要的。該函數分配給它,所以它必須是本地名稱。

請記住,整個文件是在執行開始之前編譯的,但是該函數是在執行階段定義的,當編譯的函數定義塊被執行時,創建函數對象。

複雜的優化器可以消除無法訪問的代碼,但CPython的優化器不是那麼聰明 - 它只執行非常簡單的鎖孔優化。

要深入瞭解Python內部,請查看ast和dis模塊。

+0

請你詳細說明一下。 –

+0

'x = 3無法訪問的事實是無關緊要的。函數賦給它,所以它必須是本地名稱。「所以你說'locals()'不會顯示x,因爲它只是一個定義的名稱,並沒有賦值? (或類似的東西) –

+0

@vikash因爲變量在創建之前不存在。而且由於這個任務永遠不會執行,所以從未發生。 –

4

函數中使用的名稱對於整個函數體只能有一個範圍。範圍在編譯時確定(不是在函數運行時)。

如果在函數的任何位置有一個名稱賦值(無論函數調用時是否運行),編譯器將默認將該名稱視爲本地函數。您可以使用globalnonlocal語句明確指示它使用不同的作用域。

一個特殊情況是名稱被分配給一個函數的主體,並從第一個函數中定義的另一個函數訪問。這樣一個變量將被放入一個特殊的closure單元中,這些單元將在這些函數之間共享。外部函數將像變量一樣處理變量,而內部函數只能在名稱爲nonlocal的語句中賦值。這裏的和封閉的一個例子的nonlocal聲明:

def counter(): 
    val = 0 
    def helper(): 
     nonlocal val 
     val += 1 
     return val 
    return helper 

除了你看到的問題,還有另一種類型的範圍混亂,你可能會看到:

x = 1 
def foo(): 
    print(x) # you might expect this to print the global x, but it raises an exception 
    x = 2  # this assignment makes the compiler treat the name x as local to the function 

foo功能名稱x被視爲本地無處不在,即使print調用在它已被分配到本地名稱空間之前嘗試使用它。

+1

謝謝你提到關閉。 –