Python做用域詳述

做用域是指變量的生效範圍,例如本地變量、全局變量描述的就是不一樣的生效範圍。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

上面的代碼將輸出三、一、1。解釋參見再述做用域規則。另外,我的建議,本文最後一小節內容儘可能理解透徹。多線程

python做用域規則簡介

它有4個層次的做用域範圍:內部嵌套函數、包含內部嵌套函數的函數自身、全局做用域、內置做用域。上面4個做用域的範圍排序是按照從內到外,從小到大排序的。閉包

其中:app

  • 內置做用域是預先定義好的,在__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
相關文章
相關標籤/搜索