1. 程式人生 > >Python3基礎-Python作用域詳述

Python3基礎-Python作用域詳述

轉載文章

轉載文章
作者:駿馬金龍
出處:http://www.cnblogs.com/f-ck-need-u/p/9925021.html

Python作用域詳述

作用域是指變數的生效範圍,例如本地變數、全域性變數描述的就是不同的生效範圍。

python的變數作用域的規則非常簡單,可以說是所有語言中最直觀、最容易理解的作用域。

在開始介紹作用域之前,先拋一個問題:

x=1
def f():
    x=3
    g()
    print("f:",x)   # 3

def g():
    print("g:",x)   # 1

f()
print("main:",x)    # 1

上面的程式碼將輸出3、1、1。解釋參見再述作用域規則

python作用域規則簡介

它有4個層次的作用域範圍:內部巢狀函式、包含內部巢狀函式的函式自身、全域性作用域、內建作用域。上面4個作用域的範圍排序是按照從內到外,從小到大排序的。

其中:

  • 內建作用域是預先定義好的,在__builtins__模組中。這些名稱主要是一些關鍵字,例如open、range、quit等
  • 全域性作用域是檔案級別的,或者說是模組級別的,每個py檔案中處於頂層的變數都是全域性作用域範圍內的變數
  • 本地作用域是函式內部屬於本函式的作用範圍,因為函式可以巢狀函式,巢狀的內層函式有自身的內層範圍
  • 巢狀函式的本地作用域是屬於內層函式的範圍,不屬於外層

所以對於下面這段python程式碼來說,如果它處於a.py檔案中,且沒有巢狀在其它函式內:

X=1
def out1(i):
    X=2
    Y='a'
    print(X)
    print(i)
    def in1(n):
        print(n)
        print(X,Y)
    in1(3)
out1(2)

那麼:
處於全域性作用域範圍的變數有:X、out1
處於out1本地作用域範圍的變數有:i、X、Y、in1
處於巢狀在函式out1內部的函式in1的本地作用域範圍的變數有:n

注意上面的函式名out1和in1也是一種變數

如下圖所示:

搜尋規則

當在某個範圍引用某個變數的時候,將從它所在的層次開始搜尋變數是否存在,不存在則向外層繼續搜尋。搜尋到了,則立即停止

例如函式ab()中嵌套了一個函式cd(),cd()中有一個語句print(x),它將首先檢查cd()函式的本地作用域內是否有x,如果沒有則繼續檢查外部函式ab()的本地作用域範圍內是否有x,如果沒有則再次向外搜尋全域性範圍內的變數x,如果還是沒有,則繼續搜尋內建作用域,像"x"這種變數名,在內建作用域範圍內是不存在的,所以最終沒有搜尋到,報錯。如果一開始在cd()中就已經找到了變數x,就不會再搜尋ab()範圍以及更外層的範圍。

所以,內層範圍可以引用外層範圍的變數,外層範圍不包括內層範圍的變數

內建作用域

內建作用域主要是一些內建的函式名、內建的異常等關鍵字。例如open,range,quit等。

兩種方式可以搜尋內建作用域:一是直接匯入builtins模組,二是讓python自動搜尋。匯入builtins模組會讓內建作用域內的變數直接置於當前檔案的全域性範圍,自動搜尋內建作用域則是最後的階段進行搜尋。

一般來說無需手動匯入builtins模組,不過可以看看這個模組中包含了哪些內建變數。

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', ...............
'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

變數掩蓋和修改規則

如果在函式內部引用了一個和全域性變數同名的變數,且不是重新定義、重新賦值(其實python中沒有變數宣告的概念,只有賦值的概念),那麼函式內部引用的是全域性變數。

例如,下面的函式g()中,print函式中的變數x並未在g()中單獨定義或賦值,所以這個x引用的是全域性變數x,它將輸出值3。

x=3
def g():
    print(x)  # 引用全域性變數x

如果函式內部重新賦值了一個和全域性變數名稱相同的變數,則這個變數是本地變數,它會掩蓋全域性變數。注意是掩蓋而非覆蓋,掩蓋的意思是出了函式的範圍(函式退出),全域性變數就會恢復。或者換句話說,在函式內部看到的是本地變數x=2,在函式外部看到的是全域性變數x=3

例如:下面的g()中重新聲明瞭x,這個x稱為g()的本地變數,全域性變數x=3暫時被掩蓋(當然,這是對該函式來說的掩蓋)。

x=3
def g():
    x=2   # 定義並賦值本地變數x
    print(x)  # 引用本地變數x

python是一種解釋性語言,讀一行解釋一行,讀了下一行就忘記前一行(詳細見下文)。所以在使用變數之前必須先進行變數的定義(宣告)

例如下面是錯誤的:

def g():
    print(x)
    x=3
g()

錯誤資訊:

UnboundLocalError: local variable 'x' referenced
before assignment

這個很好理解,但是下面和同名的全域性變數混合的時候,就不那麼容易理解了:

x=1
def g():
    print(x)
    x=2
g()

這裡也會報錯,而不是輸出x=1或2。

這裡需要解釋一下,雖說python是逐行解釋的。但每個函式屬於一個區塊,這個區塊範圍是一次性解釋的,並不會讀一行忘記一行,而是一直讀,讀完整個區塊再解釋。所以,讀完整個g()區塊後,首先就記住了重新定義了本地變數x=2,於是g()中所有使用變數x的時候,都是本地變數x,所以print(x)中的x也是本地變數,但這違反了使用變數前先賦值的規則,所以也會報錯。

因此,在函式內修改和全域性變數同名的變數前,必須先修改,再使用該變數。所以,上面的程式碼中,x=2必須放在print的前面:

x=1
def g():
    x=2
    print(x)
g()

所以,對於函式來說,也必須先定義函式,再呼叫函式。下面是錯誤的:

g()
def g():
    x=2
    print(x)

報錯資訊:

NameError: name 'g' is not defined

但是下面的程式碼中,f()中先呼叫了g(),然後才定義g(),為什麼能執行呢:

x=1
def f():
    x=3
    g()
    print("f:",x)   # 3

def g():
    print("g:",x)   # 1

f()
print("main:",x)    # 1

實際上並非是先呼叫了g(),python解釋到def f()區塊的時候,只是宣告這一個函式,並非呼叫這個函式,真正呼叫f()的時候是在def g()區塊的後面,所以實際上是先宣告完f()和g()之後,再呼叫f()和g()的。

但如果把f()放在def f()def g()的中間,將會報錯,因為呼叫f()函式的時候,def g()還沒解釋到,也就是說g()還沒有宣告。

x=1
def f():
    x=3
    g()
    print("f:",x)

f()   # 報錯

def g():
    print("g:",x) 

更容易犯錯的一種情況是邊賦值,邊使用。下面的程式碼是錯誤的:

x=3

def f1():
    x += 3
    print(x)
f1()

因為x += 3也是賦值操作,函式內部只要是賦值操作就表示宣告為本地變數。它首先計算x=x+3右邊的x+3,然後將結果賦值給左邊的變數x,但計算x+3的時候變數x並未定義,所以它是錯誤的。錯誤資訊:

UnboundLocalError: local variable 'x' referenced before assignment

關於全域性變數

關於python中的全域性變數:

  • 每個py檔案(模組)都有一個自己的全域性範圍
  • 檔案內部頂層的,不在def區塊內部的變數,都是全域性變數
  • def內部宣告(賦值)的變數預設是本地變數,要想讓其變成全域性變數,需要使用global關鍵字宣告
  • def內部如果沒有宣告(賦值)某變數,則引用的這個變數是全域性變數

預設情況下,下面f()中的x變數是全域性變數:

x=2
def f():
    print(x)
f()             # 輸出2

預設情況下,下面f()中的x變數是本地變數:

x=2
def f():
    x=3
    print(x)
f()        # 輸出3
print(x)   # 輸出2

global關鍵字

如果想要在def的內部修改全域性變數,就需要使用global關鍵字宣告變數:

x=2
def f():
    global x
    x=3
    print(x)

f()          # 輸出3
print(x)     # 輸出3

global可以宣告一個或多個變數為全域性變數,多個變數使用逗號隔開,也可以宣告事先不存在的變數為全域性變數:

x=2
def f():
    global x,y
    x,y = 3,4
    print(x,y)
f()
print(x,y)

不能global中直接賦值。所以下面的是錯的:

global x=2

注意,global不是宣告變數,在變數賦值之前,變數是一定不存在的,就算是被global修飾了也一樣不存在,所以下面的程式碼是錯的。實際上,global有點類似於宣告變數的名稱空間,而非變數。

x=2
def f():
    global x,y
    print(y)

報錯資訊:

NameError: name 'y' is not defined

必須在print(y)之前(不能是之後)加上y的賦值語句,才表示它的存在。

x=2
def f():
    global x,y
    y=3
    print(y)

global修飾的變數必須在它的賦值之前,所以下面的是錯的,因為y=2首先將它宣告為本地變量了。

def f():
    y=2
    global y

全域性變數的不安全性

考慮下面這個問題:

x=2
def f():
    global x
    x=3

def g():
    global x
    x=4

f()或g()
print(x)

這時,函式f()和g()的呼叫順序決定了print輸出的全域性變數x的值。因為全域性變數是共享的,如果多執行緒同時執行這段程式碼,就不知道是誰先誰後修改,導致print(x)的值隨機性。這是多執行緒不安全特性。因此,如果允許,應儘量不要將函式內部的變數修飾為全域性變數。

訪問其它模組中的全域性變數

python中一個檔案一個模組,在模組1中可以匯入模組2中的屬性(例如全域性變數)。

例如,b.py檔案中:

x=3

a.py檔案中:

import b
print(b.x)
b.x=4

上面的a.py中匯入了b.py模組,通過b.x可以訪問、修改這個來自於b.py中的全域性變數x。

這是極不安全的,因為誰也不知道是否有其他的模組也在修改b.x

所以,沒有人會去直接修改其它模組的屬性,如果要修改,基本上都會通過類似面向物件中的setter函式進行修改。只需在b.py中定義一個函式,以後在其它檔案中使用這個函式修改即可。

b.py檔案中:

x=3
def setx(n)
    global x
    x=n

a.py檔案中:

import b
b.setx(54)   # 將b.x變數設定為54

其它訪問全域性變數的方法

上面通過import匯入模組檔案,就可以獲取這個模組中屬性的訪問權。實際上,也可以在當前模組檔案中使用import mod_name匯入當前模組,其中mod_name為當前檔名,這樣就可以在函式內部直接訪問全域性變數,而無需使用global關鍵字。

除了import mod_name可以匯入當前模組,使用sys模組的modules()函式也可以匯入:sys.modules['mod_name']

例如,在b.py檔案中:

x=3

def f():
    global x
    x += 2

def f1():
    x=4        # 本地變數

def f2():
    x=4             # 本地變數
    import b
    b.x += 2  # 全域性變數

def f3():
    x=4            # 本地變數
    import sys
    glob = sys.modules['b']
    glob.x += 2    # 全域性變數

def test():
    print("aaa",x)            # 輸出3
    f();f1();f2();f3()
    print("bbb",x)            # 輸出9

在a.py檔案中:

import b
b.test()

nonlocal關鍵字

當函式進行巢狀的時候,內層函式的作用域是最內層的,它的外層是外層函式的作用域。內層函式和外層函式的關係類似於本地作用域與全域性作用域的關係:

  • (1).內層函式中賦值的變數是屬於內層、不屬於外層的本地變數
  • (2).內層函式中使用的未在當前內層函式中賦值的變數是屬於外層、全域性的變數

例如,下面的巢狀程式碼中,f2()中print(x,y)的x是屬於外層函式f1()的本地變數,而y則是屬於內層函式自身的本地變數,外層函式f1()無法訪問屬於內層函式的y。

x=3

def f1():
    x=4
    def f2():
        y=5
        print(x,y)
    f2()
f1()

nonlocal語句可以修飾內層函式中的變數使其成為它上一層函式的變數。它的用法和global基本相同,修飾多個變數的時候,需要逗號隔開。但和global有一點不同,global修飾的變數可能事先並未存在於全域性作用域內,但nonlocal修飾的變數必須已經存在於上層或上上層(或更多層)函式,不能只存在於全域性(見下面示例)。

例如下面的程式碼片段中嵌套了2次,其中f3()中的x使用nonlocal修飾,使得這個x變成它上一層作用域f2()中的x變數。

x=3

def f1():
    x=4          # f1的本地變數
    def f2():
        x=5      # f2的本地變數
        def f3():
            nonlocal x  # f2的本地變數
            print("f3:",x)  # 輸出5
            x=6
        f3()
        print("f2:",x)  # 被修改,輸出6
    f2()
f1()

上面的程式碼將輸出:

f3: 5
f2: 6

如果將上面的f2()中的x=5刪除,會如何?

x=3

def f1():
    x=4
    def f2():
        def f3():
            nonlocal x      # f1()的本地
            print("f3:",x)  # 輸出4
            x=6             # 修改f1()的本地
        f3()
        print("f2:",x)   # 輸出6
    f2()
    print("f1:",x)       # 輸出6
f1()

注意,上面f3()中的nonlocal將x修飾為f1()的本地變數,因為f3()的上一層f2()中沒有變數x,所以f2()繼承了f1()的變數x,使得f3()修改上一層f2()中的變數,等價於修改f1()中的變數x。

但如果把f1()中的x=4也刪除,那麼將報錯,因為nonlocal無法將變數修飾為全域性範圍。

所以,nonlocal預設將內層函式中的變數修飾為上一層函式的作用域範圍,如果上一層函式中不存在該變數,則修飾為上上層、上上上層直到頂層函式,但不能修飾為全域性作用域範圍

同樣的,只要在內層函式中賦值,就表示宣告這個變數的作用域為內層函式作用域範圍。所以,下面的程式碼是錯誤的:

x=3
def f1():
    x=4
    def f2():
        print(x)
        x=3
    f2()
f1()

下面的程式碼也是錯的:

x=3
def f1():
    x=4
    def f2():
        x += 3
        print(x)
    f2()
f1()

錯誤資訊:

UnboundLocalError: local variable 'x' referenced before assignment

至於原因,前文已經解釋的很清楚。

訪問外層函式變數的其它方法

在以前的版本中,還沒有nonlocal關鍵字,這時如果要儲存外層函式的變數,就需要使用函式引數預設值的方式定義內層函式。

x=3
def f1():
    x=4
    def f2(x=x):
        x += 3
        print("f2:",x)
    x=5
    f2()
    print("f1:",x)
f1()

輸出:

f2: 7
f1: 5

上面的f2(x=x)中,等號右邊的x來自於f1()中x=4,然後將其賦值給f2()的本地作用域變數x。注意,python的作用域是詞法作用域,函式區塊的定義位置決定了它看到的變數。所以,儘管呼叫f2()之前再次對x進行了賦值,f2()函式呼叫時,f2(x=x)等號右邊的x早已經賦值給左邊的本地變數x了。它們的關係如下圖所示:

避免函式巢狀

一般來說,函式巢狀都只用於閉包(工廠函式),而且是結合匿名函式(lambda)實現的閉包。其它時候,函式巢狀一般都可以改寫為非巢狀模式。

例如,下面的巢狀函式:

def f1():
    x=3
    def f2():
        nonlocal x
        print(x)
    f2()
f1()

可以改寫為:

def f1():
    x=3
    f2(x)

def f2(x):
    print(x)

f1()

迴圈內部的函式

當函式位於迴圈結構中,且這個函式引用了迴圈控制變數,那麼結果可能會出乎意料。

本來以匿名函式(lambda)來解釋更清晰,但因為尚未介紹匿名函式,所以這裡採用命名函式為例。

下面的程式碼中,將5個函式作為列表的元素儲存到列表list1中。

def f1():
    list1 = []
    for i in range(5):
        def n(x):
            return i+x
        list1.append(n)
    return list1

mylist = f1()
for i in mylist: print(i)
print(mylist[0](2))
print(mylist[2](2))

結果:

<function f1.<locals>.n at 0x02F93660>
<function f1.<locals>.n at 0x02F934B0>
<function f1.<locals>.n at 0x02F936A8>
<function f1.<locals>.n at 0x02F93738>
<function f1.<locals>.n at 0x02F93780>
6
6

從結果中可以看到mylist[0](2)mylist[2](2)的執行結果是一樣的,不僅如此,mylist[N](2)的結果也全都一樣。換句話說,儲存到列表中的各個函式n()中所引用的迴圈控制變數"i"並沒有因為迴圈的迭代而改變,而且列表中所有函式儲存的i的值都是迴圈的最後一個元素i=4

(注:對於此現象,各語言基本都是如此,本節稍作解釋,真正的本質原因在本文的最後一節做了額外的補充解釋程式碼塊細述)。

先看下面的例子:

def f1():
    for i in range(5):
        def n():
            print(i)
    return n

f1()()

結果輸出4。可見,print(i)的值並沒有隨迴圈的迭代過程而改變。

究其原因,是因為def n()只是函式的宣告,它不會去查詢i的值是多少,所以不會將i的值替換到函式n()的i變數,而是直接儲存變數i的地址,當迴圈結束時,i指向最後一個元素i=4的地址。

當開始呼叫n()的時候,即f1()(),才會真正開始查詢i的值,這時候i指向的正是i=4。

這就像下面的程式碼一樣,在還沒有開始呼叫f()的時候,f()內部的x一直都只是指向它所看見的變數x,而這個x是全域性作用域範圍。當真正開始呼叫f()的時候,才會去定位x的指向

x=3
def f():
    print(x)

回到上面迴圈中的巢狀函式,如果要保證迴圈的迭代能作用到其內部的函式中,可以採用預設引數值的方式進行賦值:

def f1():
    list1 = []
    for i in range(5):
        def n(x,i=i):
            return i+x
        list1.append(n)
    return list1

上面def n(x,i=i)中的i=i是設定預設引數值,等號右邊的i是函式宣告時就查詢並替換完成的,所以每次迴圈迭代過程中,等號右邊的i都不同,等號左邊的引數i的預設值就不同。

再述作用域規則

python的作用域是詞法作用域,這意味著函式的定義位置決定了它所看見的變數。除了詞法作用域,還有動態作用域,動態作用域意味著函式的呼叫位置決定了它所看見的變數。關於詞法、動態作用域,本文不多做解釋,想要了解的話,可以參考一文搞懂:詞法作用域、動態作用域、回撥函式、閉包

下面是本文開頭的問題:

x=1
def f():
    x=3
    g()
    print("f:",x)   # 3

def g():
    print("g:",x)   # 1

f()
print("main:",x)    # 1

對於python的這段程式碼來說,這裡有兩個值得注意的地方:

  1. 呼叫函式之前,理論上要先定義好函式,但這裡g()的呼叫似乎看上去比g()的定義更先
  2. f()中呼叫g()時,為什麼g()輸出的是1而不是3

第一個問題在前文已經解釋過了,再解釋一遍:雖然f()裡面有g()的呼叫語句,但def f()只是宣告,但在呼叫f()之前,是不會去呼叫g()的。所以,只要f()的呼叫語句在def g()之後,就是正確的。

第二個問題,python是詞法作用域,所以:

  • (1).首先宣告def f(),在此期間會建立一個本地變數x,並且print("f:",x)中的x指向這個本地變數;
  • (2).然後宣告g(),在此期間,g()的定義語句不在f()內部,而是在全域性範圍,所以它看見的是x是全域性x,所以print("g:",x)中的x指向全域性變數x;

當呼叫f()的時候,執行到g()時,g()中所指向的是全域性範圍的x,而非f()段中的x。所以,輸出1。

再看一個巢狀在函式內部的示例:

x=3

def f1():
    x=4
    def f2():
        print(x)
    x=5
    f2()
f1()      # 輸出5

這裡的問題是f2()中的print為什麼不輸出4,而是輸出5。

其實也很容易理解,因為def f2()是定義在f1()內部的,所以f2()看見的x是f1()中的x,也就是說print(x)中的x指向的是f1()中的x。但在呼叫f2()之前,重新賦值了x=5,等到呼叫f2()的時候,根據x的指向,將找到新的x的值。

也就是說,前面的示例中,有兩個獨立的變數x:全域性的和f()本地的。後面這個示例中只有一個變數x,屬於f()。

程式碼塊細述(必看)

程式碼塊可以使得一段python程式碼作為一個單元、一個整體執行。以下是 官方手冊 的描述。

A Python program is constructed from code blocks. A block is a piece of Python program text that is executed as a unit. The following are blocks: a module, a function body, and a class definition. Each command typed interactively is a block. A script file (a file given as standard input to the interpreter or specified as a command line argument to the interpreter) is a code block. A script command (a command specified on the interpreter command line with the ‘-c’ option) is a code block. The string argument passed to the built-in functions eval() and exec() is a code block.

所以,有以下幾種型別的程式碼塊:

  1. 模組檔案是一個程式碼塊
  2. 函式體是一個程式碼塊
  3. class的定義是一個程式碼塊
  4. 互動式(python idle)的每一個命令列都是一個獨立的程式碼塊
  5. 指令碼檔案是一個程式碼塊
  6. 指令碼命令是一個程式碼塊(python -c "xxx")
  7. eval()和exec()中的內容也都有各自的程式碼塊

程式碼塊的作用是組織程式碼,同時意味著退出程式碼區塊範圍就退出了作用域範圍。例如退出函式區塊,就退出了函式的作用域,使得函式內的本地變數無法被函式的外界訪問。

此外,python是解釋性語言,讀一行解釋一行,這意味著每讀一行就忘記前一行。但實際上更嚴格的說法是讀一個程式碼塊解釋一個程式碼塊,這意味著讀程式碼塊中的內容時,是暫時記住屬於這個程式碼塊中所讀內容的,讀完整個程式碼塊後再以統籌的形式解釋這個程式碼塊。

先說明讀一行解釋一行的情況,也就是每一行都屬於一個程式碼塊,這個只能通過python的互動式工具idle工具來測試:

>>> x=2000
>>> y=2000
>>> x is y
False
>>> x=2000;y=2000
>>> x is y
True

理論上分號是語句的分隔符,並不會影響結果。但為什麼第一個x is y為False,而第二個x is y為True?

首先分析第一個x is y。由於互動式工具idle中每一個命令都是一個單獨的語句塊,這使得解釋完x=2000後立刻就忘記了2000這個數值物件,同時忘記的還有x變數本身。然後再讀取解釋y=2000,因為不記得剛才解釋的x=2000,所以會在記憶體中重新建立一個數值結構用來儲存2000這個數值,然後用y指向它。換句話說,x和y所指向的2000在記憶體中是不同的資料物件,所以x is y為False。

下面的x is y返回True:

>>> x=2000;y=2000
>>> x is y
True

因為python按行解釋,一個命令是一個程式碼塊。對於x=2000;y=2000,python首先讀取這一整行,發現x和y的數值物件都是2000,於是做個簡單優化,等價於x,y=2000,2000,這意味著它們屬於一個程式碼塊內,由於都是2000,所以只會在記憶體中建立一個數據物件,然後x和y都引用這個資料物件。所以,x is y返回True。

idle工具中每個命令都是獨立的程式碼塊,但是py檔案卻是一個完整的程式碼塊,其內還可以巢狀其它程式碼塊(如函式、exec()等)。所以,如果上面的分行賦值語句放在py檔案中,得到的結果將是True。

例如:

x = 2000
y = 2000
print(x is y)   # True
def f1():
    z=2000
    z1=2000
    print(x is z)   # False
    print(z is z1)  # True

f1()

python先讀取x=2000,並在記憶體中建立一個屬於全域性作用域的2000資料物件,再解釋y=2000的時候,發現這個全域性物件2000已經存在了(因為x和y同處於全域性程式碼塊內),所以不會再額外建立新的2000物件。這裡反映出來的結果是"同一個程式碼塊內,雖然仍然是讀一行解釋一行,但在退出這個程式碼塊之前,不會忘記這個程式碼塊中的內容,而且會統籌安排這個程式碼塊"。

同理def f1()內的程式碼塊,因為z是本地作用域的變數,更標準的是處於不同程式碼塊內,所以會在本地作用域記憶體區建立新的資料物件2000,所以x is z返回False。根據前面的解釋,z1 is z返回True。

再回顧前文多次出現的一個異常:

x = 3
def f1():
    print(x)
    x=4
f1()

報錯資訊:

UnboundLocalError: local variable 'x' referenced before assignment

當執行到def語句的時候,因為def宣告函式,函式體是一個程式碼塊,所以按照程式碼塊的方式讀取屬於這個程式碼塊中的內容。首先讀取print(x),但並不會直接解釋,而是會記住它,並繼續向下讀取,於是讀取x=4,這意味著x是一個本地變數。然後統籌安排整個程式碼塊,將print(x)的x認為是本地變數而非全域性變數。注意,直到def退出的時候都還沒有進行x的賦值,而是記錄了本地變數x,賦值操作是在函式呼叫的時候進行的。當呼叫函式f()的時候,發現print(x)中的x是本地變數,但因為還沒有賦值,所以報錯。

但是再看下面的,為什麼又返回True?

>>> x=256
>>> y=256
>>> x is y
True

因為Python在啟動的時候就在記憶體中預先為常用的較小整數值(-5到256)建立好了物件,因為它們使用的非常頻繁(有些在python的內部已經使用了)。所以,對於這個範圍內的整數,都是直接引用,不會再在記憶體中額外建立新的數值物件,所以x is y總是返回true。甚至,這些小值整數可以跨作用域:

x = 3
def f1():
    y=3
    print(x is y)   # True

f1()

再看前文迴圈內的函式的問題。

def f1():
    for i in range(5):
        def n():
            print(i)
    return n

f1()()

前面對現象已經解釋過,內部函式n()中print(i)的i不會隨迴圈的迭代而改變,而是固定的值i=4。

python首先解釋def f1()的程式碼塊,會記錄屬於這個程式碼塊作用域內的變數i和n,但i和n都不會賦值,也就是說暫時並不知道變數n是一個函式變數。

同理,當需要解釋def n()程式碼塊的時候,將記住這個程式碼塊涉及到的變數i,只不過這個變數i是屬於外層函式的,但不管如何,這個程式碼塊記住了i,且記住了它是外部函式作用域的。

注意,函式的宣告過程中,所有涉及到變數i的作用域內都不會對i進行賦值,僅僅只是儲存了這個i變數名,只有在呼叫函式的時候才會進行賦值操作

當開始呼叫f1()的時候,開始執行函式體中的程式碼,於是開始迴圈迭代,且多次宣告函式n(),每一次迭代生成的n()都會讓原先已記錄的變數n指向這個新宣告的函式體(相當於賦值的操作,只不過是變數n引用的物件是函式體結構,而不是一般的資料物件),由於只是在迴圈中宣告函式n(),並沒有進行呼叫,所以不會對n()中的i進行賦值操作。而且,每次迴圈迭代都會讓變數n指向新的函式體,使得先前迭代過程中定義的函式被丟棄(覆蓋),所以最終只記住了最後一輪迴圈時宣告的函式n(),並且i=4。

當呼叫f1()()時,表示呼叫f1()中返回的函式n(),直到這個時候才會對n()內的i進行賦值,賦值時將搜尋它的外層函式f1()作用域,發現這個作用域內的i指向記憶體中的數值4,於是最終輸出4。

再看下面的程式碼:

def f1():
    for i in range(5):
        def n():
            print(i)
        n()
    return n

f1()

輸出結果:

0
1
2
3
4

呼叫f1()的時候,執行迴圈的迭代,每次迭代時都會呼叫n(),意味著每次迭代都要對n()中的i進行賦值。

另外注意,前面說過,函式的預設引數是在函式宣告時進行賦值的,所以下面的列表L中每個元素所代表的函式,它們的變數i都指向不同的數值物件。

def f1():
    L = []
    for i in range(5):
        def n(i=i):
            print(i)
        L.append(n)
    return L

f1()[0]()
f1()[1]()
f1()[2]()
f1()[3]()
f1()[4]()

執行結果:

0
1
2
3
4