Python 的命名空間

懶得掃全文的童鞋,能夠直接跳到最後看總結。
咱們先從一個簡單的栗子提及:python

栗子

a 文件中有變量 va 以及類 A,b 文件導入 aclass A ,並打印出 Asegmentfault

#a.py
va = ['dobi', 'a', 'dog']

print('a1', id(va))

class A():
    def __init__(self):
        pass

    def rtn(self):
        global va
        va.insert(1,'is')
        print('a3', id(va))
        return va

print('a2', va)


#b.py
from a import A

print('b', A)

執行 b 文件的結果爲:閉包

Reloaded modules: a
a1 2407907960200
a2 ['dobi', 'a', 'dog']
b <class 'a.A'>

能夠發現,雖然 b 只是導入了 a 中的 class A,但導入這個過程卻執行了整個 a 文件,那麼咱們是否可以在 b 中訪問 a 中的全局變量 va 呢:app

print(va)
# NameError: name 'va' is not defined
print(a.va)
# NameError: name 'a' is not defined
print(b.va)
# NameError: name 'b' is not defined

嘗試了各種調用方法,發現都沒法正常訪問 a 的全局變量 va,既然 b 的導入執行了整個 a 文件,甚至還打印出了 vaid 和值,又爲何沒法在 b 中調用 va 呢?ide

這個問題所涉及到的內容就是:命名空間。函數

但在開始正題以前,咱們須要闡明若干概念:ui

一些基本概念的澄清

對象

Python 一切皆對象,每一個對象都具備 一個ID、一個類型、一個值;對象一旦創建,ID 便不會改變,能夠直觀的認爲 ID 就是對象在內存中的地址spa

a = [1, 2]
b = a
id(a)
# 2407907978632
id(b)
# 2407907978632
b[1] = 3
a
# [1, 3]

上例 a, b 共享了同一個 ID、同一個值、同一個類型。所以 a, b 表達的是同一個對象,但 a, b 又明顯是不一樣的,好比一個叫 'a' 一個叫 'b'...既然是同一個對象,爲何又有不一樣的名字呢?難道名字不是對象的屬性?設計

標識符

事實確實如此,這是 Python 比較特殊一點:如同'a' 'b' 這樣的名稱其實有一個共同的名字:identifier(注意不要與 ID 混淆了),中文名爲「標識符」,來解釋一下:code

標識符:各種對象的名稱,好比函數名、方法名、類名,變量名、常量名等。

在 Python 中賦值並不會直接複製數據,而只是將名稱綁定到對象,對象自己是不知道也不須要關心(該關心這個的是程序猿)本身叫什麼名字的。一個對象甚至能夠指向不一樣的標識符,上例中的'a' 'b'即是如此。真正管理這些名子的事物就是本文的主角「命名空間」。

命名空間

命名空間(Namespace):名字(標識符)到對象的映射。

簡而言之,命名空間能夠理解爲:記錄對象和對象名字對應關係的空間;現今 Python 的大部分命名空間是經過字典來實現的,也即一個命名空間就是名字到對象的映射,標識符是鍵,對象則是值。

做用域

與命名空間相對的一個概念就是「做用域」,那麼什麼又是做用域呢?

做用域(Scope):本質是一塊文本區域,Python 經過該文本區域能夠直接訪問相應的命名空間。

這裏須要搞清楚什麼是直接訪問:

#x.py
a = 1
class A():
    def func():pass
python x.py
a   #直接訪問
# 1
A.func  #屬性訪問

Python 中不加 . 的訪問爲直接訪問,反之爲屬性訪問。

所以能夠簡單的將做用域理解爲「直接訪問命名空間的一種實現」,具體而言:

  1. 做用域內相應的命名空間能夠被直接訪問;

  2. 只有做用域內的命名空間才能夠被直接訪問(所以並非全部的命名空間均可以被直接訪問)。

看不懂? 不要緊,後面會解釋,如今先回到命名空間這個話題上,咱們常常接觸的命名空間有四類:

LEGB

LEGB 命名空間

這四類命名空間能夠簡記爲 LEGB:

  1. 局部命名空間(local):指的是一個函數或者一個類所定義的名稱空間;包括函數的參數、局部變量、類的屬性等。

  2. 閉包命名空間(enclosing function):閉包函數 的名稱空間(Python 3 引入)。

  3. 全局命名空間(global):讀入一個模塊(也即一個.py文檔)後產生的名稱空間。

  4. 內建命名空間(builtin):Python 解釋器啓動時自動載入__built__模塊後所造成的名稱空間;諸如 str/list/dict...等內置對象的名稱就處於這裏。

爲了說清楚這幾類命名空間,舉個栗子:

#c.py
v1 = 'a global var'

def func(v):
    v2 = 'a local var'
    def inn_func():
        v3 = v2 + v
        return v3
    return inn_func

內建命名空間比較好理解,咱們重點講解下其餘三個:

  1. 'v1' 爲全局變量 v1 的名子,其所處的命名空間爲全局命名空間;須要注意的是全局命名空間包括 'func' 但不包括 func 的參數和內部變量。

  2. func 囊括 'v''v2''inn_func' 名稱的空間爲局部命名空間;

  3. 執行 func 後,func 的做用域釋放(或許遺忘更合適),並返回了綁定了 vv2 變量的閉包函數 inn_func,此時閉包所具備命名空間即爲閉包命名空間,所以局部命名空間和閉包命名空間是相對而言的,對於父函數 func 而言,二者具備產生時間上的差別。

LEGB 訪問規則

經過上面描述,咱們發現 LEGB 四類命名空間自己具備明顯的內外層級概念,而這種層級概念正是構建做用域的前提:做用域依據這種層級概念將不一樣類型的命名空間組織起來並劃歸到不一樣層級的做用域,而後定義好不一樣層級做用域之間的訪問規則,從而實現命名空間的直接訪問:
LEGB 訪問規則: 一樣的標識符在各層命名空間中能夠被重複使用而不會發生衝突,但 Python 尋找一個標識符的過程老是從當前層開始逐層往上找,直到首次找到這個標識符爲止

#d.py
v1 = 1
v2 = 3
def f():
    v1 = 2
    print(1, v1)
    print(2, v2)

f()
print(3, v1)
1 2
2 3
3 1

上例中,全局變量和函數 f 都定義了 變量 v1,結果 Python 會優先選擇 f 的局部變量 v1 ,對於 f 內並未定義的變量 v2 ,Python 會向上搜尋全局命名空間,讀取全局變量 v2 後打印輸出。

global 和 nonlocal 語句

global 和 nonlocal 的做用

如前所述,對於上層變量,python 容許直接讀取,可是卻不能夠在內層做用域直接改寫上層變量,來看一個典型的閉包結構:

#e.py
gv = ['a', 'global', 'var']

def func(v):
    gv = ['gv'] + gv #UnboundLocalError:local variable 'gv' referenced before assignment
    lv = []
    def inn_func():
        lv = lv + [v]  #UnboundLocalError:local variable 'lv' referenced before assignment
        gv.insert(1, lv[0])
        return gv
    return inn_func

實際調用 func()函數後,上面兩處對 gvlv 進行賦值操做的地方都會發生 UnboundLocalError:由於 Python 在執行函數前,會首先生成各層命名空間和做用域,所以 Python 在執行賦值前會將func 內的 'gv' 'lv' 寫入局部命名空間和閉包命名空間,當 Python 執行賦值時會在局部做用域、閉包做用域內發現局部命名空間和閉包命名空間內已經具備'gv''lv' 標識符,但這兩個非全局標識符在該賦值語句執行以前並無被賦值,也即沒有對象與標識符關聯,所以沒法參與四則運算,從而引起錯誤;但這段程序本意可能只是想讓具備對象的全局變量gv 和局部變量 lv 參與運算,爲了不相似的狀況發生,Python 便引入了 globalnonlocal 語句就來講明所修飾的 gvlv 分別來自全局命名空間和局部命名空間,聲明以後,就能夠在 funcinn_func 內直接改寫上層命名空間內 gvlv 的值:

#f.py
gv = ['a', 'global', 'var']

def func(v):
    global gv
    gv = ['gv'] + gv
    lv = []
    print(id(lv))
    def inn_func():
        nonlocal lv
        lv = lv + [v]
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func
a = func('is')
# 2608229974344

a()
# 2608229974344
# ['gv', 'is', 'a', 'global', 'var']

print(gv)
# ['gv', 'is', 'a', 'global', 'var']

如上,全局變量 gv 值被函數改寫了, inn_func 修改的也確實是父函數 lv的值 (依據 ID 判斷)。

借殼

那麼是否是不使用 globalnonlocal 就不能達到上面的目的呢?來看看這段程序:

#g.py
gv = ['a', 'global', 'var']

def func(v):
    gv.insert(0, 'gv')
    lv = []
    print(id(lv))
    def inn_func():
        lv.append(v)
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func

執行的結果:

a = func('is')
# 2608110869168

a()
# 2608110869168
# ['gv', 'is', 'a', 'global', 'var']

print(gv)
# ['gv', 'is', 'a', 'global', 'var']

能夠發現,執行結果同上面徹底一致,問題天然來了:「爲何不用 global nonlocal 也能夠改寫全局變量gv和父函數變量lv的值?

爲了看清楚這個過程,咱們將上面的gv.insert(0, 'gv') lv.append(v) 改寫爲 gv[0:0] = ['gv'] lv[:] = [v]:

#h.py
gv = ['a', 'global', 'var']

def func(v):
    gv[0:0] = ['gv']
    lv = []
    print(id(lv))
    def inn_func():
        lv[:] = [v]
        print(id(lv))
        gv.insert(1, lv[0])
        return gv
    return inn_func

執行結果:

a = func('is')
# 2608229959496

a()
# 2608229959496
# ['gv', 'is', 'a', 'global', 'var']

同 g.py 文件的執行結果徹底一致,事實上二者之間的內在也是徹底同樣的。
So 咱們其實改寫的不是 gvlv ,而是 gvlv 的元素 gv[0:0]lv[:] 。所以,不須要 globalnonlocal 修飾就能夠直接改寫,這就是「借殼」,nonlocal 還沒有引入 Python 中,好比 Python 2.x 若要在子函數中改寫父函數變量的值就得經過這種方法。
固然借殼蘊藏着一個相對複雜的標識符建立的問題:好比子函數經過借殼修改父函數變量lv的值,那麼子函數的標識符lv是怎麼綁定到父函數變量lv的值 ID 的上的?

關於這個問題,這裏有個問答就是討論這個的:python的嵌套函數中局部做用域問題?

global 和 nonlocal 語句對標識符建立的不一樣影響

另外,須要注意的是:global 語句只是聲明該標識符引用的變量來自於全局變量,但並不能直接在當前層建立該標識符;nonlocal 語句則會在子函數命名空間中建立與父函數變量同名的標識符:

#j.py
gv = 'a global var'

def func():
    global gv
    lv = 'a local var'
    print(locals())
    def inn_func():
        nonlocal lv
        global gv
        print(locals())
    return inn_func

執行結果:

c = func()
{'lv': 'a local var'}   #運行 `func` 函數後,`global` 語句並未將 `gv` 變量引入局部命名空間

c()
{'lv': 'a local var'}   #運行閉包函數後,`nonlocal` 語句將父函數變量 `lv` 引入閉包命名空間

之因此 nonlocal 語句與 global 語句的處置不一樣,在於全局變量的做用域生存期很長,在模塊內隨時均可以訪問,而父函數的局部做用域在父函數執行完畢後便會直接釋放,所以 nonlocal 語句必須將父函數變量的標識符和引用寫入閉包命名空間。

命名空間的生命週期

建立規則

實際上,到這裏其實還有一個重要的重要問題沒有解決:「標識符並非天生就在命名空間內的,命名空間也不是無緣無故就產生的,那麼命名空間是在何時被建立?又是在何時被刪除的呢?」
規則有四:

  1. 內建命名空間在 Python 解釋器啓動時建立,以後會一直存在;

  2. 模塊的全局命名空間在模塊定義被讀入時建立,一般模塊命名空間也會保持到解釋器退出。

  3. 函數調用時產生新的局部命名空間;函數返回結果、拋出異常時釋放命名空間,每一次遞歸都生成一個命名空間。

  4. 標識符產生地點決定標識符所處的命名空間。

這四點就是拿來秒懂的!不過,仍然有一點經常被忽視:類的命名空間:

類的局部命名空間

首先,函數和類執行時都會產生局部命名空間,但類的執行機制不一樣於函數:

#i.py
def a():
    print('function')

class A():
    print(1)
    class B():
        print(2)
        class C():
            print(3)

執行文件,結果爲:

1
2
3

如上,類就是一個可執行的代碼塊,只要該類被加載,就會被執行,這一點不一樣於函數。
類之因此這麼設計的緣由在於:類是建立其餘實例(生成其餘的類或者具體的對象)的對象,所以必須在實例以前被建立,而類又可能涉及到與其餘類的繼承、重載等一系列問題,故在代碼加載時就被建立利於提升效率和下降邏輯複雜度。

其次,與函數不一樣的是,類的局部命名空間並不是做用域

class A():
    a = 1
    b = [a + i for i in range(3)]  #NameError: name 'a' is not defined

執行上段代碼,咱們能夠發如今類 A 內列表推導式沒法調取 a 的值,但函數卻能夠:

def func():
    a = 1
    b = [a + i for i in range(3)]
    print(b)

func()  #[1, 2, 3]

所以,A 中的 a 不一樣於函數 func 中的 a 在局部命名空間中能夠被任意讀取,之因此說是「不能夠被任意」讀取而不是「不可被讀取」,緣由在於在類A 的局部空間內,a 其實必定程度上是能夠直接被讀取的:

class A():
    a = 1
    c = a + 2

執行上段代碼後:

A.c 
#3

而上例中 b 的賦值操做不能執行,緣由在於列表推導式會建立本身的局部命名空間,所以難以訪問到 a

編譯與局部命名空間

Python 是動態語言,不少行爲是動態發生的,但 Python 自身也在不斷進步,好比爲了提升效率,有些行爲會在編譯時候完成,局部變量的建立就是如此:

def func():
    a = 1
    def inn_func():
        print(a)  # error
        a = 2     # error
    inn_func()

上段程序還未執行,就提示存在有語法錯誤,緣由在於python 解釋器發現 inn_func 內存在自身的 a 變量,但卻在聲明以前就被 print 了。

總結

囉嗦了這麼多,終於該結尾了!
咱們再來回過頭來看下文章開頭的栗子:
一、爲何 b.py 只是導入 a.py 中的 class A,卻執行了整個 a.py 文件?
答:由於 Python 並不知道 class A 在 a.py 文檔的何處,爲了可以找到 class A,Python 須要執行整個文檔。
二、爲何 b.py 的導入執行了整個 a.py 文檔,卻在 b 中難以調用 a 的全局變量 va
答:Python 的全局變量指的是模塊全局,所以不能夠跨文檔,所以 global 語句也是不能夠跨文檔的。另外, b 只是導入了 a 的 class A,所以並不會導入 a 中全部的標識符,因此 相似a.va 這樣的調用也是不起做用的。

關於命名空間:
一、賦值、定義類和函數都會產生新的標識符;
二、全局變量的標識符不能跨文檔;
三、各級命名空間相互獨立互不影響;
四、Python 老是從當前層逐漸向上尋找標識符;
五、內層做用域若想直接修改上層變量,須要經過 global nonlocal 語句先聲明;
六、單純的 global 語句並不能爲所在層級建立相應標識符,但 nonlocal 語句能夠在閉包空間中建立相應標識符;七、類的局部命名空間不是做用域。

相關文章
相關標籤/搜索