關於這個系列
《最值得收藏的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就是一個自由變量。