Python進階 - 命名空間與作用域

来源:http://www.cnblogs.com/crazyrunning/archive/2017/05/27/6914080.html
-Advertisement-
Play Games

本文詳細講解Python的命名空間,作用域,以及在使用中的一些常見困惑。 ...


Python進階 - 命名空間與作用域

寫在前面

如非特別說明,下文均基於Python3

命名空間與作用於跟名字的綁定相關性很大,可以結合另一篇介紹Python名字、對象及其綁定的文章

1. 命名空間

1.1 什麼是命名空間

Namespace命名空間,也稱名字空間,是從名字到對象的映射。Python中,大部分的命名空間都是由字典來實現的,但是本文的不會涉及命名空間的實現。

命名空間的一大作用是避免名字衝突:

def fun1():
    i = 1

def fun2():
    i = 2

同一個模塊中的兩個函數中,兩個同名名字i之間絕沒有任何關係,因為它們分屬於不同明明空間。

1.2 命名空間的種類

常見的命名空間有:

  • built-in名字集合,包括像abs()這樣的函數,以及內置的異常名字等。通常,使用內置這個詞表示這個命名空間-內置命名空間

  • 模塊全局名字集合,直接定義在模塊中的名字,如類,函數,導入的其他模塊等。通常,使用全局命名空間表示。

  • 函數調用過程中的名字集合,函數中的參數,函數體定義的名字等,在函數調用時被“激活”,構成了一個命名空間。通常,使用局部命名空間表示。

  • 一個對象的屬性集合,也構成了一個命名空間。但通常使用objname.attrname的間接方式訪問屬性,而不是直接訪問,故不將其列入命名空間討論。

  • 類定義的命名空間,通常解釋器進入類定義時,即執行到class ClassName:語句,會新建一個命名空間。(見官方對類定義的說明)

1.3 命名空間的生命周期

不同類型的命名空間有不同的生命周期:

  • 內置命名空間,在Python解釋器啟動時創建,解釋器退出時銷毀;

  • 全局命名空間,模塊的全局命名空間在模塊定義被解釋器讀入時創建,解釋器退出時銷毀;

  • 局部命名空間,這裡要區分函數以及類定義。函數的局部命名空間,在函數調用時創建,函數返回或者由未捕獲的異常時銷毀;類定義的命名空間,在解釋器讀到類定義創建,類定義結束後銷毀。(關於類定義的命名空間,在類定義結束後銷毀,但其實類對象就是這個命名空間內容的包裝,見官方對類定義的說明)

2. 作用域

2.1 什麼是作用域

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

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

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

2.2 與命名空間的關係

現在,命名空間持有了名字。作用域是Python的一塊文本區域,即一塊代碼區域,需要代碼區域引用名字(訪問變數),那麼必然作用域與命名空間之間就有了聯繫。

顧名思義,名字作用域就是名字可以影響到的代碼文本區域,命名空間的作用域就是這個命名空間可以影響到的代碼文本區域。那麼也存在這樣一個代碼文本區域,多個命名空間可以影響到它。
作用域只是文本區域,其定義是靜態的;而名字空間卻是動態的,只有隨著解釋器的執行,命名空間才會產生。那麼,在靜態的作用域中訪問動態命名空間中的名字,造成了作用域使用的動態性。

那麼,可以這樣認為:

靜態的作用域,是一個或多個命名空間按照一定規則疊加影響代碼區域;運行時動態的作用域,是按照特定層次組合起來的命名空間。

在一定程度上,可以認為動態的作用域就是命名空間。在後面的表述中,我會把動態的作用域與其對應命名空間等同起來。

2.3 名字搜索規則

在程式中引用了一個名字,Python是怎樣搜索到這個名字呢?

在程式運行時,至少存在三個命名空間可以被直接訪問的作用域:

  • Local
    首先搜索,包含局部名字的最內層(innermost)作用域,如函數/方法/類的內部局部作用域;

  • Enclosing
    根據嵌套層次從內到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封閉函數的作用域。如兩個嵌套的函數,內層函數的作用域是局部作用域,外層函數作用域就是內層函數的 Enclosing作用域;

  • Global
    倒數第二次被搜索,包含當前模塊全局名字的作用域;

  • Built-in
    最後被搜索,包含內建名字的最外層作用域。

程式運行時,LGB三個作用域是一定存在的,E作用域不一定存在;若程式是這樣的:

i = 1
print(i)

局部作用域在哪裡呢?我們認為(Python Scopes And Namespaces):

Usually, the local scope references the local names of the (textually) current function. Outside functions, the local scope references the same namespace as the global scope: the module’s namespace. Class definitions place yet another namespace in the local scope.

一般地,局部作用域引用函數中定義的名字。函數之外,局部作用域和全局作用域引用同一個命名空間:模塊的明星空間。然而類型的局部作用域引用了類定義新的命名空間。

Python按照以上L-E-G-B的順序依次在四個作用域搜索名字。沒有搜索到時,Python拋出NameError異常。

2.4 何時引入作用域

我們知道:

Python中一個名字只有在定義之後,才能引用。

print(i)

直接引用未定義的名字i,按照搜索規則,在LGB三個作用域均沒有搜索到名字i(LB相同命名空間)。拋出NameError異常:

Traceback (most recent call last):
  File "scope_test.py", line 15, in <module>
    print(i)
NameError: name 'i' is not defined

那對於這段代碼呢?

def try_to_define_name():
    '''函數中定義了名字i,並綁定了一個整數對象1'''
    i = 1

try_to_define_name()
print(i) #引用名字i之前,調用了函數

在引用名字i之前,明明調用了函數,定義了名字i,可是還是找不到這個名字:

Traceback (most recent call last):
  File "scope_test.py", line 20, in <module>
    print(i) #引用名字i之前,調用了函數
NameError: name 'i' is not defined

雖然定義了名字i,但是定義在了函數的局部作用域對應的局部命名空間中,按照LEGB搜索規則,在全局作用域中自然訪問不到局部作用域;再者,函數調用結束後,這個命名空間被銷毀了。

引用名字總是與作用域相關的,因此:

Python中一個名字只有在定義之後,才能在合適的作用域引用。

那麼,在定義名字時,就要註意名字定義的作用域了,以免定義後需要訪問時卻找不到。所以,瞭解Python在何時會引入新的作用域很有必要。一般來說,B,G兩個作用域的引入在不能夠通過代碼操作的,能夠通過語句引入的作用域只有E,L了。Python中引入新作用域的語句很有限,總的來說只有兩類一個:

  • 函數定義引入local作用域或者Enclosing作用域;本質上,lambda和生成器表達式也是函數,會引入新作用域。
  • 類定義引入local作用域;
  • 列表推導式引入local作用域,傳說在python2中列表推導式不引入新的作用域

幾個會讓有其他高級語言經驗的猿困惑的地方:

if語句

if True:
    i = 1
print(i) # output: 1,而不是NameError

if語句並不會引入新的作用域,所以名字綁定語句i = 1print(i)是在同一個作用域中。

for語句

for i in range(6):
    pass
print(i) #output: 5,而不是NameError

for語句同樣不會引入新的作用域,所以名字i的綁定和重綁定與print(i)在同一個作用域。這一點Python就比較坑了,因此寫代碼時切忌for迴圈名字要與其他名字不重名才行。

import語句

def import_sys():
    '''import sys module'''
    import sys

import_sys()
print(sys.path) # NameError: name 'sys' is not defined

這個算非正常程式員的寫法了,在另一篇文章《Python進階 - 對象,名字以及綁定》中介紹過,import語句在函數import_sys中將名字sys和對應模塊綁定,那sys這個名字還是定義在局部作用域,跟上面的例子沒有任務區別。要時刻切記Python的名字,對象,這個其他編程語言不一樣,但是:

打破第一編程語言認知的第二門編程語言,才是值得去學的好語言。

3. 作用域應用

3.1 自由變數可讀不可寫

我不太想用“變數”這個詞形容名字,奈何變數是家喻戶曉了,Python中的自由變數:

If a variable is used in a code block but not defined there, it is a free variable.

如果引用發生的代碼塊不是其定義的地方,它就是一個自由變數。專業一點,就是:

引用名字的作用域中沒有這個名字,那這個名字就是自由名字

Note: “自由名字”只是作者YY的,並沒得到廣泛認可。

我們已經瞭解了作用域有LEGB的層次,並按順序搜索名字。按照搜索順序,當低層作用域不存在待搜索名字時,引用高層作用域存在的名字,也就是自由名字:
[示例1]

def low_scope():
    print(s)

s = 'upper scope'
low_scope()

很清楚,這段代碼的輸出是upper scope
[示例2]

def low_scope():
    s = 'lower scope'

s = 'upper scope'
low_scope()
print(s)

很遺憾,最後的列印語句沒有按照期待列印出lower scope而是列印了upper scope

A special quirk of Python is that – if no global statement is in effect – assignments to names always go into the innermost scope.

Python的一個怪癖是,如果沒有使用global語句,對名字的賦值語句通常會影響最內層作用域。
即賦值語句影響局部作用域,賦值語句帶來的影響是綁定或重綁定,但是在當前局部作用域的命名空間中,並沒有s這個名字,因此賦值語句在局部作用於定義了同名名字s,這與外層作用域中的s並不衝突,因為它們分屬不同命名空間。
這樣,全局作用域的s沒有被重綁定,結果就很好解釋了。

當涉及可變對象時,情況又有所不同了:
[示例3]

def low_scope():
    l[0] = 2

l = [1, 2]
low_scope()
print(l) # [2, 2]

很遺憾,最後的列印語句沒有按照期待輸出[1, 2]而是輸出了[2, 2]
上一個例子的經驗並不能運用在此,因為list作為一個可變對象,l[0] = 2並不是對名字l的重綁定,而是對l的第一個元素的重綁定,所以沒有新的名字被定義。因此在函數中成功更新了全局作用於中l所引用對象的值。

註意,下麵的示例跟上面的是不一樣的:
[示例4]

def low_scope():
    l = [2, 2]

l = [1, 2]
low_scope()
print(l) # [1, 2]

我們可以用本節中示例1的方法解釋它。

綜上,可以認為:

自由變數可讀不可寫。

3.2 globalnonlocal

總是存在打破規則的需求:

在低層作用域中需要重綁定高層作用功能變數名稱字,即通過自由名字重綁定。

於是global語句和nonlocal語句因運而生。

global_stmt ::= "global" identifier ("," identifier)*
The global statement is a declaration which holds for the entire current code block. It means that the listed identifiers are to be interpreted as globals. It would be impossible to assign to a global variable without global, although free variables may refer to globals without being declared global.

global語句是適用於當前代碼塊的聲明語句。列出的標識符被解釋為全局名字。雖然自由名字可以不被聲明為global就能引用全局名字,但是不使用global關鍵字綁定全局名字是不可能的。

nonlocal_stmt ::= "nonlocal" identifier ("," identifier)*
The nonlocal statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals. This is important because the default behavior for binding is to search the local namespace first. The statement allows encapsulated code to rebind variables outside of the local scope besides the global (module) scope.

nonlocal語句使得列出的名字指向最近封閉函數中綁定的名字,而不是全局名字。預設的綁定行為會首先搜索局部作用域。nonlocal語句使得在內層函數中重綁定外層函數作用域中的名字成為可能,即使同名的名字存在於全局作用域。

經典的官方示例:

def scope_test():
    
    def do_local():
        spam = 'local spam'

    def do_nonlocal():
        nonlocal spam # 當外層作用域不存在spam名字時,nonlocal不能像global那樣自作主張定義一個
        spam = 'nonlocal spam' # 自由名字spam經nonlocal聲明後,可以做重綁定操作了,可寫的。

    def do_global():
        global spam # 即使全局作用域中沒有名字spam的定義,這個語句也能在全局作用域定義名字spam
        spam = 'global spam' # 自有變數spam經global聲明後,可以做重綁定操作了,可寫的。

    spam = 'test spam'
    do_local()
    print("After local assignment:", spam) # After local assignment: test spam
    do_nonlocal()
    print("After nonlocal assignment:", spam) # After nonlocal assignment: nonlocal spam
    do_global()
    print("After global assignment:", spam) # After global assignment: nonlocal spam


scope_test()
print("In global scope:", spam) # In global scope: global spam

作者說不行nonlocal的邪:

def nest_outter():
    spam = 'outer'

    def nest_inner():
        nonlocal spam1
        spam1 = 'inner'

    nest_inner()
    print(spam)

nest_outter()

Output:

  File "scope_test.py", line 41
    nonlocal spam1
SyntaxError: no binding for nonlocal 'spam1' found

4. 一些坑

作者曾經自信滿滿認為透徹瞭解了Python的作用域,但是一大堆坑踩得觸不及防。

一臉懵逼

4.1 坑1 - UnboundLocalError
def test():
    print(i)
    i = 1

i = 2
test()

Output:

Traceback (most recent call last):
  File "scope_test.py", line 42, in <module>
    test()
  File "scope_test.py", line 38, in test
    print(i)
UnboundLocalError: local variable 'i' referenced before assignment

其實忽略掉全局作用域中i = 2這條語句,都可以理解。

Usually, the local scope references the local names of the (textually) current function.

Python對局部作用域情有獨鍾,解釋器執行到print(i)i在局部作用域沒有。解釋器嘗試繼續執行後面定義了名字i,解釋器就認為代碼在定義之前就是用了名字,所以拋出了這個異常。如果解釋器解釋完整個函數都沒有找到名字i,那就會沿著搜索鏈LEGB往上找了,最後找不到拋出NameError異常。

4.2 坑2 - 類的局部作用域
class Test(object):

    i = 1

    def test_print(self):
        print(i)

t = Test()
i = 2
t.test_print()

我就問問大家,這個輸出什麼?
當然會出乎意料輸出2了,特別是有其他語言經驗的人會更加困惑。

上文強調過:
函數命名空間的生命周期是什麼? 調用開始,返回或者異常結束,雖然示例中是調用的方法,但其本質是調用類的函數。
類命名空間的作用域是什麼?類定義開始,類完成定義結束。

類定義開始時,創建新的屬於類的命名空間,用作局部作用域。類定義完後,命名空間銷毀,沒有直接方法訪問到類中的i了(除非通過間接訪問的方式:Test.i)。

方法調用的本質是函數調用:

class Test(object):

    i = 1

    def test_print(self):
        print(i)

t = Test()
i = 2
# t.test_print()
Test.test_print(t) # 方法調用最後轉換成函數調用的方式

函數調用開始,其作用域與全局作用域有了上下級關係(LG),函數中i作為自由名字,最後輸出2
因此,不能被類中數據成員和函數成員的位置迷惑,始終切記,Python中兩種訪問引用的方式:

  • 直接引用:試圖直接寫名字name引用名字,Python按照搜索LEGB作用域的方式搜索名字。

  • 間接引用:使用objname.attrname的方式引用名字attrnamePython不搜索作用域,直接去對象里找屬性。

4.3 坑3 - 列表推導式的局部作用域

一個正常列表推導式:

a = 1
b = [a + i for i in range(10)]
print(b) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

現在把列表推導式放到類中:

class Test(object):

    a = 1
    b = [a + i for i in range(10)]
    print(b)

    def test(self):
        pass

Output:

Traceback (most recent call last):
  File "scope_test.py", line 15, in <module>
    class Test(object):
  File "scope_test.py", line 18, in Test
    b = [a + i for i in range(10)]
  File "scope_test.py", line 18, in <listcomp>
    b = [a + i for i in range(10)]
NameError: name 'a' is not defined

輸出反饋名字a未定義。

上文強調過,解釋器讀取類定義開始class ClassName後,創建命名空間用作局部作用域
語句a = 1,在這個局部作用域中定義了名字i
語句b = [a + i for i in rage(10)],列表推導式同樣創建了一個局部作用域。這個作用域與類定義的局部作用域並沒有上下級關係,所以,自然沒有任何直接訪問名字a的方法。

Python中只有四種作用域:LEGB,因為類定義的局部作用域與列表推導式的局部作用域於不是嵌套函數關係,所以並不能構成Enclosing作用域關係。因此它們是兩個獨立的局部作用域,不能相互訪問。

既然是兩個獨立局部作用域,那麼上述例子就等同於:

def test1():
    i = 1

def test2():
    print(i)

test1()
test2()

期待在test2中訪問test1的名字i,顯然是不可行的。

參考

  1. Python Scopes and Namespaces
  2. Python命名空間和作用域窺探
  3. Python的作用域
  4. Naming and binding

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 題目 synchronized怎麼實現線程同步?請修改《每天一道Java題[10]》中的MyRunnableThread類以解決三個線程都獲取到10的問題。 解答 方法一: 採用synchronized關鍵字包裹需要保證線程安全的代碼塊,來實現線程同步。語法格式為: 《每天一道Java題[10]》中 ...
  • 1.什麼是適配器模式? 適配器模式是一種過渡模式,用於溝通兩個不相容的事物,實現信息交換。 2.適配器模式的目的 使一個對象能夠以一種相對簡單的方式處理多個不同類型的對象,即一個對象相容多個不同類型的對象。例如,電腦接收外部硬體的插口唯一確定,不同尺寸的記憶體卡先插到讀卡器上,再由讀卡器插到唯一確定的 ...
  • 題目描述 在一個園形操場的四周擺放N堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的2堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。 試設計出1個演算法,計算出將N堆石子合併成1堆的最小得分和最大得分. 輸入輸出格式 輸入格式: 數據的第1行試正整數N,1≤N≤100,表示有N ...
  • 轉載請出自出處:http://www.cnblogs.com/hd3013779515/ 1.Redis安裝 使用的最新版本為 3.2.9,下載並安裝: 執行make後報錯 從錯誤看原因是缺少gcc,執行yum install gcc。之後再次執行make,還是報錯。 執行make distclea ...
  • 1682. [HAOI2014]貼海報 ★★☆ 輸入文件:ha14d.in 輸出文件:ha14d.out 簡單對比 時間限制:1 s 記憶體限制:256 MB 【題目描述】 Bytetown城市要進行市長競選,所有的選民可以暢所欲言地對競選市長的候選人發表言論。為了統一管理,城市委員會為選民準備了一個 ...
  • ★★ 輸入文件:counttree.in 輸出文件:counttree.out 簡單對比 時間限制:1 s 記憶體限制:128 MB 【題目描述】 【輸入格式】 輸入第一行包含一個整數N,以下N行每行包含一個整數,其中第i行的整數表示編號為i的節點的父親節點的編號,根的父親節點編號為0。 【輸出格式】 ...
  • 誤解一:JavaScript是Java的簡易版 JavaScript是一種在網頁中使用的腳本語言,它的原名叫做LiveScript。JavaScript的語法與Java類似。除此之外,他們再無任何關係。JavaScript的一個子集已經標準化為ECMA-262,它更加緊密地與瀏覽器集成在一起。 誤解 ...
  • 一、MFC的概念和作用 1、什麼是MFC? 全稱:Microsoft Foundation Class Library(微軟基礎類庫) 1-MFC從硬碟存在形式來說就是一個庫(靜態MFC庫、動態MFC庫) 2-MFC從原理來說還是一個程式框架 2、為什麼使用MFC? 基於框架編程,提高工作效率,減少 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...