Python函數中的作用域規則


Python是靜態作用域語言,儘管它本身就是一個動態語言。也就是說,在Python中變數的作用域是由它在原始碼中的位置決定的,這與C有些相似,但是Python與C在作用域方面的差異還是非常明顯的。


接下來會談論Python的作用域規則,在這中間也會說明Python與C在作用域上的不同。

在Python 2.0及之前的版本中,Python只支援3種作用域,即局部作用域,全域作用域,內建作用域;在Python 2.2中,Python正式引入了一種新的作用域--- 嵌套作用域;在Python 2.1中,嵌套作用域可以作為一個選項被開啟;嵌套作用域的引入,本質上為Python實現了對閉包的支持,關於閉包的知識,網路上有很多解釋,這裡就不詳細展開了。相應地,變數查找順序由先前的LGB變成LEGB(L:Local,E:Enclosing,G:Global,B:Built-in)。


在Python中,並不是任何程式碼區塊都能引入新的作用域,這與C有很大的不同:


#include<stdio.h>int main() {    if(2 > 0) {        int i = 0;
    }
    printf("i = %d", i);    return 0;
}

在這段程式碼中,if子句引入了一個局部作用域,變數i就存在於這個局部作用域中,但對外不可見,因此,接下來在printf函數中對變數i的引用會引發編譯錯誤。

但是,在Python中卻並非如此:

if True:
    i = 0print i

#在這段程式碼中,if子句並沒有引入一個局部作用域,變數i仍然處在全域作用域中,因此,變數i對於接下來的print語句是可見的。

實際上,在Python中,只有模組,類別以及函數才會引入新的作用域,其它的程式碼區塊是不會引入新的作用域的。

在Python中,使用一個變數之前不必預先聲明它,但是在真正使用它之前,它必須已經綁定到某個物件;而名字綁定將在當前作用域中引入新的變量,同時屏蔽外層作用域中的同名變量,不論這個名字綁定發生在目前作用域中的哪個位置。 

def f():    print i
f()


運行結果會顯示:NameError: global name 'i' is not defined。 Python先在函數f的本地作用域中尋找變數i,尋找失敗,接著在全域作用域和內建作用域中尋找變數i,仍然失敗,最後拋出NameError異常。

i = 0def f():
    i = 8    print i
f()print i


運行結果顯示:8和0。 i = 8是名字綁定運算,它在函數f的局部作用域中引入了新的變數i,屏蔽了全域變數i,因此f內部的print語句看到的是局部變數i,f外部的print語句看到的是全域變數i。

i = 0def f():    print i
    i = 0
f()


運行結果顯示:UnboundLocalError: local variable 'i' referenced before assignment。在這個例子當中,函數f中的變量i是局部變量,但是在print語句使用它的時候,它還未被綁定到任何物件之上,所以拋出異常。

print i
i = 0


不論是以互動的方式運行,還是以腳本檔案的方式運行,結果都顯示:NameError: name 'i' is not defined。這裡的輸出結果又與上一個例子不同,這是因為它在頂級作用域(模組作用域)的緣故。對於模組程式碼而言,程式碼在執行之前,沒有經過任何預處理,但是對於函數體而言,程式碼在運行之前已經經過了一個預處理,因此不論名字綁定發生在作用域的那個位置,它都能感知出來。 Python雖然是靜態作用域語言,但是名字查找確實動態發生的,因此直到運行的時候,才會發現名字方面的問題。


在Python中,名字綁定在所屬作用域中引入新的變量,同時綁定到一個物件。名字綁定發生在以下幾種情況之下:


  1. #參數宣告:參數宣告在函數的局部作用域中引入新的變數;
  2. 賦值運算:對變數進行初次賦值會在目前作用域中引入新的變量,後續賦值運算則會重新綁定該變數;
  3. 類別和函數定義:類別和函數定義將類別名稱和函數名稱作為變數引入目前作用域,類別體和函數體將形成另外一個作用域;
  4. #import語句:import語句在目前作用域中引入新的變量,一般是在全域作用域;
  5. for語句:for語句在目前作用域中引入新的變數(循環變數);
  6. except語句:except語句在目前作用域中引入新的變數(例外物件)。


在Python中,類別定義所引入的作用域對於成員函數是不可見的,這與C 或Java是很不同的,因此在Python中,成員函數想要引用類別體定義的變量,必須透過self或類別名稱來引用它。


巢狀作用域的加入,會導致一些程式碼編譯不過或得到不同的運行結果,在這裡Python解釋器會幫助你識別這些可能會引起問題的地方,給予警告。


locals函數傳回所有的局部變量,但不會傳回巢狀作用域中的變量,實際上沒有函數會傳回巢狀作用域中的變數。


#