《最值得收藏的python3語法彙總》之命名空間和作用域

關於這個系列

《最值得收藏的python3語法彙總》,是我爲了準備公衆號“跟哥一起學python”上面視頻教程而寫的課件。整個課件將近200頁,10w字,幾乎囊括了python3所有的語法知識點。

你可以關注這個公衆號“跟哥一起學python”,獲取對應的視頻和實例源碼。

這是我和幾位老程序員一起維護的個人公衆號,全是原創性的乾貨編程類技術文章,歡迎關注。


命名空間(namespace)是對符號(變量、函數、類等等)名字的一種分組機制,它提供了在項目中避免名字衝突的一種方法。不同組的相同命名符號被視作兩個獨立的符號,因此隸屬於不同命名空間的符號名稱可以重複。

命名空間是高級編程語言的一種基本概念,C++、Java等都支持命名空間機制。C語言不支持命名空間,所以C語言的程序員需要自己保證命名在全局範圍內不重複。

在Python中,命名空間採用了字典的結構來實現,它記錄了符號名稱和對象之間的對應關係。

Python有4種命名空間:

  • 局部命名空間(Local):函數中定義的名稱,記錄了函數的變量,包括函數的參數和局部定義的變量。(類中定義的也是)。
  • 閉合命名空間(Enclosing):外部嵌套函數的名字空間(例如closure)。
  • 全局命名空間(Global):模塊(.py)中定義的名稱,記錄了模塊的變量,包括函數、類、其它導入的模塊、模塊級的變量和常量。
  • 內置命名空間(Built-in): Python 語言內置的名稱,比如函數名 abs、char 和異常名稱 BaseException、Exception 等等。

 

需要注意的是,上圖中的命名空間並沒有從上到下的包含關係,比如:我們不能認爲Global-ns是包含它下級的Local-ns的。事實上,所有的命名空間都是獨立存在的。

 

 

 什麼是作用域(scope)?

作用域是Python的一塊文本區域,這個區域中,命名空間可以被“直接訪問”。這裏的直接訪問指的是試圖在命名空間中找到名字的絕對引用(非限定引用)。這裏有必要解釋下直接引用和間接引用:

直接引用:直接使用名字訪問的方式,如name,這種方式嘗試在名字空間中搜索名字name。

間接引用:使用形如objname.attrname的方式,即屬性引用,這種方式不會在命名空間中搜索名字attrname,而是搜索名字objname,再訪問其屬性。

 

上面的四種命名空間的作用域如下:

  • 局部命名空間(Local):它的作用域僅限於本地,比如本函數、本類等。
  • 閉合命名空間(Enclosing):它的作用域僅限被嵌套的下層函數。
  • 全局命名空間(Global):它的作用域是整個模塊(.py)。
  • 內置命名空間(Built-in): 它的作用域是整個python解釋器運行實例。

我們可以看到,這些命名空間的作用域是相互包含的。

 

當解釋器要查找一個符號名字時,它會採用L-E-G-B(由近及遠)的順序,依次在這四種命名空間中去查找。

我們看看下面的例子:


#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./11/11_1.py
# 命名空間和作用域

x = 1  # 位於Global-NS

def foo():
    x = 2  # 位於foo的Local-NS,相對於innerfoo來說,是外層嵌套函數NS,Enclosing-NS
    def innerfoo():
        x = 3  # 位於Local-NS
        print('locals', x)
    innerfoo()
    print('enclosing function locals ', x)

foo()
print('global ', x)

輸出爲:

locals 3

enclosing function locals  2

global  1

如果要執行innerfoo函數,查找變量x時,按照LEGB原則,獲取到的是Local-NS中的x對應的對象。

這裏需要注意的是,Local-NS和Enclosing-NS的概念是相對的。對於innerfoo來說,foo的Local-NS也是innerfoo的Enclosing-NS。

需要特別注意的是,Python中,只有模塊、函數、類、推導式等可以產生作用域,而if、for、while等等這些控制語句,則不會產生作用域。這和其他編程語言存在差異。比如下面這個簡單的例子:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./11/11_2.py
# if不產生作用域

x = 100

if True:
    x = 200

print(x)

這個例子中,x被改變了,最後輸出結果爲200。所以if語句中使用的x變量就是Glocal-NS的中的名字x。if本身並沒有產生一個新的作用域。

 

理解變量的定義和引用?

我們需要清楚地理解變量的定義和引用這兩個行爲的區別。

比如: x = 100,這是定義了變量x (define); 而print(x),這是引用了變量x。

對於引用變量的行爲,python會採用LEGB原則到命名空間去查找名字,如果找不到則拋出異常;

在Python中,任何符號(變量名、函數名、類名等等)都是需要先定義後才能被引用的。

對於定義變量的行爲,python會在對應的命名空間增加名字和對象的映射關係。同時要注意一點,定義行爲是優先於引用行爲的

我們再回頭看看前面的例子,我們只看函數foo定義的代碼片段:

def foo():

    x = 2
    def innerfoo():
        x = 3 
        print('locals', x)
    innerfoo()
    print('enclosing function locals ', x)

內層函數innerfoo中第一行代碼x=3,它到底是新定義了一個Local-NS的變量名x呢?還是引用了外層函數foo的變量名x呢?根據我們前面提到的原則:定義行爲優先於引用行爲。所以,解釋器會優先認爲它是新定義的一個Local-NS變量名x,並添加到命名空間中。下一行print函數引用變量x時,更加LEGB順序,首先查找到的就是這個新定義的變量名x。

這個例子中的x是一個不可變數據類型,我們再看一個可變數據類型,它的行爲會讓你感到更加“怪異”!

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./11/11_3.py
# 變量的定義和引用

list_1 = [100, 200]
list_2 = [100, 200]

def foo1():
    list_1 = [300]  # 這是定義

def foo2():
    list_2.append(300)  # 這是引用

foo1()
foo2()
print(list_1)
print(list_2)

輸出爲:

[100, 200]

[100, 200, 300]

List_1和list_2產生的結果是完全不一樣的,foo1中是新定義了一個Local-NS的變量list_1,而foo2中是對Global-NS中list_2的引用。

通過前面的例子,我們可以看到,如果在作用域裏面使用=號賦值運算符,那麼解釋器會優先認爲是定義行爲。那麼如果我們要用=號修改外部變量,該怎麼辦呢?

我們需要使用到global和nonlocal關鍵字。

Global:告訴解釋器,我使用的這個變量是Global-NS裏面的變量。

Nonlocal:告訴解釋器,我使用的這個變量是外層嵌套函數的變量。

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./11/11_4.py
# global

list_1 = [100, 200]
def foo1():
    global list_1  # 聲明list_1使用的是全局的
    list_1 = [300]  # 這是引用,是對list_1的修改

foo1()
print(list_1)

輸出爲:

[300]

解釋器看到global聲明後,會在foo1中引用Global-NS中的list_1名字。所以,list_1=[300]不被認爲是定義行爲,因爲已經定義過了。

同樣,nonlocal也類似:

#  author: Tiger,   關注公衆號“跟哥一起學python”,ID:tiger-python

# file: ./11/11_5.py
# non-local

def foo1():
    list_1 = [500]
    def foo1_inner():

nonlocal list_1
        list_1 = [300]  # 這是引用,是對list_1的修改
    foo1_inner()
    return list_1

print(foo1())

輸出爲:

[300]

解釋器在看到nonlocal聲明後,會引用外層函數foo1定義的變量list_1。

Global和nonlocal存在一個重要的區別,nonlocal聲明的變量必須在外層函數中已經定義,否則解釋器會報錯。而global聲明的變量如果沒有定義,那麼它會主動幫你定義一個。

大家在看一些官方手冊時,還會提到一個“自由變量(free variable)”的概念。當一個變量被引用的地方不是它定義的作用域時,這個變量就叫做“自由變量”。比如上面例子中全局定義的list_1,被foo函數內部引用了,那麼list_1就是一個自由變量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章