一文了解 Python 中的命名空間和作用域

本文主要介紹 Python 的命名空間和作用域,以及 nonlocal 和 global 的用法。閱讀本文預計需要 15 min。

1. 前言

Python 命名空間(Namespace)作用域(Scope)對於所有 Python 程序員都非常有用。弄明白 Python 命名空間和作用域對於我們 Python 編程和調試 Bug 都很有用!

爲什麼需要命名空間和作用域呢?從設計層面,我目前的理解是爲了計算機正確執行代碼。有了規範協議,按照規範協議寫代碼,開發人員和計算機就能互相理解對方,默契配合,代碼也就能正確執行。把這些當做一個規範協議,符合規範的代碼,才能正確執行,不然就是 Bug 了。

本文主要內容:

  • 命名空間
  • 作用域
  • 全局變量 VS 局部變量
  • global VS nonlocal

2. 命名空間

命名空間(Namespace):指的是從名字(Name)對象(Object)映射(Mapping)。可以把它理解爲一張“映射表”

這像我們初中學的映射關係,命名空間是名字和對象的一張映射表。通過名字,我們就可以找到這個對象的位置。如同,名字是鑰匙,對象是房間,一把鑰匙打開一間房間。

命名空間是互相獨立的。關於命名空間的重要一點是,不同命名空間中的名稱之間絕對沒有關係,在不同的映射表中可以出現相同的名字,而不會出問題,例如,兩個不同的模塊都可以定義一個 maximize 函數而不會產生混淆,只要模塊的用戶在其前面加上模塊名稱。

Python 中常見的命名空間有:

  • 存放內置函數的集合(包含 abs() 這樣的函數,和內建的異常等),可以認爲是”內置函數映射表“。
  • 模塊中的全局名稱,可以認爲是“模塊映射表”。
  • 函數調用中的局部名稱,可以認爲是“函數映射表”。

在不同時刻創建的命名空間擁有不同的生命週期。:

  • 內置名稱的命名空間:是在 Python 解釋器啓動時創建的,永遠不會被刪除,直到退出 Python。如:Python 內置的關鍵字、函數等就放在這個裏面。
  • 模塊的全局命名空間:在模塊定義被讀入時創建;通常,模塊命名空間也會持續到解釋器退出。被解釋器的頂層調用執行的語句,從一個腳本文件讀取或交互式地讀取,被認爲是 __main__ 模塊調用的一部分,因此它們擁有自己的全局命名空間。如:我們定義的全局變量就放在模塊的全局命名空間。(內置名稱實際上也存在於一個模塊中;這個模塊稱作 builtins 。)
  • 函數的局部命名空間:在這個函數被調用時創建,並在函數返回或拋出一個不在函數內部處理的錯誤時被刪除。(事實上,比起描述到底發生了什麼,忘掉它更好。)當然,每次遞歸調用都會有它自己的局部命名空間。如:我們定義的局部變量就放在局部命名空間。

相信現在,大家對於命名空間有了基本的理解,簡單說,它就是一個名字和對象的映射表。一個程序運行的時候,會生成很多個映射表(命名空間),那麼這些命名空間有什麼用呢?接下來就介紹作用域。

3. 作用域

作用域(Scope): 是一個命名空間(映射表)可直接訪問的 Python 程序的文本區域。 這裏的 “可直接訪問” 意味着對名稱的非限定引用會嘗試在不同命名空間中查找名稱。

這是官方文檔給的解釋。我的理解是:把源代碼(程序)比作中國的話,命名空間(映射表)就是各級人民政府,作用域就好比各級人民政府管轄的區域(代碼文本片段),即映射表管理的代碼文本片段。如:一個函數的命名空間(映射表)管理的範圍就是這個函數代碼文本。

我們知道中央可以管理地方,地方不可以管理中央。命名空間(映射表)也是一樣的,有些命名空間(映射表)可以管理整個代碼,有些命名空間(映射表)只能管理一個函數代碼。那麼問題來了,這麼多命名空間(映射表),Python 是怎麼找到需要的對象的呢?

其實,跟政府管理時,一級一級向上報告一樣,Python 也是一級一級的不同的命名空間(映射表)查找,如果所有的映射表都找不到,就報錯。

查找的順序就是之前說過的 LEGB 原則。

作用域是靜態確定,動態使用。 在 Python 程序運行的任何時間,至少有 3 個命名空間是能夠直接訪問的作用域:

  1. 首先搜索包含局部名稱(局部變量)的最內部的作用域。這是最先查找的命名空間(映射表),可以當作村委,處在基層,最下面的位置。(Local, scope L)

  2. 從最近的閉包作用域開始搜索的任何閉包函數的作用域,如:非局部名稱(non-local 變量)、非全局名稱(non-global)等。簡單說就是存在函數嵌套時,一級一級往上層函數的命名空間查找。Enclosing scope, E)

  3. 倒數第二個作用域是包含當前模塊全局名稱(全局變量)的命名空間(映射表)。如:全局變量。(Global scope, G)

  4. 最後搜索最外面的作用域,即包含內置名稱(內置函數、關鍵字等)的命名空間(映射表)。(Builtin scope, B)

注意:類定義單獨放在一個局部作用域的命名空間。

重要的是應該意識到作用域是按代碼文本來確定的:在一個模塊內定義的函數的全局作用域就是該模塊的命名空間,無論該函數從什麼地方或以什麼別名被調用。 另一方面,實際的名稱(變量)搜索是在運行時動態完成的 — 但是,Python 正在朝着“編譯時靜態名稱解析”的方向發展,因此不要過於依賴動態名稱解析! (事實上,局部變量已經是被靜態確定了。)這部分了解一下就行。

4. 全局變量 VS 局部變量

全局變量通常是指在函數外定義的變量,它的引用和賦值都是跟模塊的命名空間(映射表)關聯,所以它的聲明生命週期和模塊一樣,直到程序結束執行,或者這個全局變量被刪除,它就從映射表中被刪除。

局部變量通常是指函數內定義的變量。這個函數被調用時,創建這個函數的命名空間(映射表),函數調用結束時刪除。

舉個栗子:

city = '武漢'  # 這是全局變量,作用域是全局,放在模塊命名空間(映射表)

def my_info():
    name = 'Jock'  # 這是局部變量,作用在局部
    print(f"打印局部變量name:{name}")
    print(f"打印全局變量city:{city}")  # 這時city變量是在模塊命名空間(映射表)找到

my_info()
print(f"在函數外打印全局變量city:{city}")

# NameError: name 'name' is not defined,因爲name變量作用在函數my_info,
# 函數調用結束,函數的命名空間被刪除,name 不存在了
# print(f"在函數外打印局部變量name:{name}")

輸出結果:
打印局部變量name:Jock
打印全局變量city:武漢
在函數外打印全局變量city:武漢

這裏說明一下,name 是局部變量,放在 my_info 函數的命名空間,只作用於函數裏面,所以無法在函數外使用。

如果在函數內嘗試修改全局變量的話,那麼 Python 會在函數內重新創建一個新的局部變量,這個變量的名字和全局變量名字一樣,只不過局部變量放在函數命名空間,全局變量放在模塊命名空間。

測試如下:

city = '武漢'  # 這是全局變量,作用域是全局,放在模塊命名空間(映射表)

def my_info():
    city = '桂林'
    print(f"函數內變量city的內存地址是:{id(city)}")
    print(f"在函數內打印變量city:{city}")

my_info()
print(f"函數外變量city的內存地址是:{id(city)}")
print(f"在函數外打印變量city:{city}")

輸出結果:
函數內變量city的內存地址是:2619120672560
在函數內打印變量city:桂林
函數外變量city的內存地址是:2619120920496
在函數外打印變量city:武漢

可以發現函數裏的變量 city 和函數外的變量 city 內存地址不一樣,說明是不同的兩個對象。

那麼如何實現在函數內修改全局變量呢?接下來就介紹兩個關鍵字 globalnonlocal

5. global VS nonlocal

global:用於聲明全局變量,可以在函數內聲明全局變量,然後實現對全局變量的修改。

舉栗子:

city = '武漢'  # 這是全局變量,作用域是全局,放在模塊命名空間(映射表)

def my_info():
    global city  # 聲明city爲全局變量
    city = '桂林'  # 這時修改的是全局變量
    print(f"函數內變量city的內存地址是:{id(city)}")
    print(f"在函數內打印變量city:{city}")

my_info()
print(f"函數外變量city的內存地址是:{id(city)}")
print(f"在函數外打印變量city:{city}")

輸出結果:
函數內變量city的內存地址是:2042908827440
在函數內打印變量city:桂林
函數外變量city的內存地址是:2042908827440
在函數外打印變量city:桂林

可以發現,成功實現了對全局變量的修改。
使用 global 有一些注意事項:

  1. global 後聲明的變量在本作用域中必須沒有被使用。global citycity = '桂林' 互換位置會報錯。這有點必須先聲明後使用的感覺。
  2. global 後聲明的變量不能用於函數的正式形參、 for 循環的控制目標、class 定義、函數定義、import 語句和變量標註。這部分的話,沒啥經驗,我也不太清楚,讀者有知道的,歡迎留言交流。

nonlocal:在嵌套函數中,在內層函數聲明 nonlocal 可以實現修改外層函數的局部變量值。

舉栗子:

def my_info():
    city = "武漢"  # 外層函數局部變量
    def my_city():
        city = "桂林"
        print(f"在內層函數變量city的內存地址是:{id(city)}")
        print(f"在內層函數打印變量city:{city}")
    my_city()
    print(f"在外層函數變量city的內存地址是:{id(city)}")
    print(f"在外層函數打印變量city:{city}")

my_info()

結果輸出:
在內層函數變量city的內存地址是:1265469990832
在內層函數打印變量city:桂林
在外層函數變量city的內存地址是:1265469742896
在外層函數打印變量city:武漢

可以發現,在嵌套函數中,外層函數中的變量,相對於內層函數來說,只可讀不可修改。在內層函數嘗試修改外層函數的變量,會在內層函數重新創建一個新的局部變量,而外層函數的變量依然存在。注意,在內部函數中不能先訪問外部函數變量,之後再定義一個同名的局部變量,這樣也會報錯。如下:

def my_info():
    city = "武漢"  # 外層函數局部變量
    def my_city():
        print(f"在內層函數訪問外層函數變量city:{city}")
        city = "桂林"
        print(f"在內層函數變量city的內存地址是:{id(city)}")
        print(f"在內層函數打印變量city:{city}")
    my_city()
    print(f"在外層函數變量city的內存地址是:{id(city)}")
    print(f"在外層函數打印變量city:{city}")

my_info()

輸出結果:
UnboundLocalError: local variable 'city' referenced before assignment

即 Python 解釋器會認爲你的局部變量沒有賦值定義就使用了,所以報錯。

那麼如何實現內部函數修改外部函數局部變量呢?通過 nonlocal 實現。

def my_info():
    city = "武漢"  # 外層函數局部變量
    def my_city():
        nonlocal  city
        city = "桂林"
        print(f"在內層函數變量city的內存地址是:{id(city)}")
        print(f"在內層函數打印變量city:{city}")
    my_city()
    print(f"在外層函數變量city的內存地址是:{id(city)}")
    print(f"在外層函數打印變量city:{city}")

my_info()

輸出結果:
在內層函數變量city的內存地址是:2628557791024
在內層函數打印變量city:桂林
在外層函數變量city的內存地址是:2628557791024
在外層函數打印變量city:桂林

可以發現,通過 nonlocal 成功實現了在內層函數修改外層函數局部變量。
使用 nonlocal 需要注意:nonlocal 後面跟着的變量必須是外層函數的局部變量,否則會報錯。SyntaxError: no binding for nonlocal 'XXX' found。如:

def my_info():
    city = "武漢"  # 外層函數局部變量
    def my_city():
        nonlocal  city
        nonlocal  name
        city = "桂林"
        print(f"在內層函數變量city的內存地址是:{id(city)}")
        print(f"在內層函數打印變量city:{city}")
    my_city()
    print(f"在外層函數變量city的內存地址是:{id(city)}")
    print(f"在外層函數打印變量city:{city}")

my_info()

輸出結果:
SyntaxError: no binding for nonlocal 'name' found

下面展示一個官方文檔給的例子,如果能夠看明白,下面的例子,相信大家已經徹底搞明白了 globalnonlocal 的用法,對於作用域和命名空間也有了更深的認識。
global 和 nonlocal 的用法:

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

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

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

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

輸出結果:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

說明:
可以看到內部函數 do_local() 不能修改外部函數 scope_test() 的局部變量,如果內部函數要修改外部函數的局部變量,需要用 nonlocal 關鍵字。通過 do_nonlocal() 的方式可以實現修改外部函數 scope_test() 的局部變量 spam,所以 spam 經過 do_nonlocal() 變爲了 nonlocal spam 。內部函數 do_global() 中用 global 關鍵字聲明瞭一個全局變量,然後對全局變量 spam 進行賦值(如果已存在全局變量 spam,則會修改原全局變量值),但是這不會影響 scope_test() 函數中局部變量 spam 的值,因爲這兩個 spam 存放在不同的命名空間,外層函數中的 spam 依然是 nonlocal spam

Python 的一個特殊規定是這樣的:如果不存在生效的 global 或 nonlocal 語句 – 則對名稱的賦值總是會進入最內層作用域。 賦值不會複製數據 — 它們只是將名稱(變量)綁定到對象。 刪除也是如此:語句 del x 會從局部作用域所引用的命名空間中移除對 x 的綁定。 有點像給一個對象貼標籤(賦值)和撕標籤(刪除)的過程。

事實上,所有引入新名稱的操作都是使用局部作用域:特別地,import 語句和函數定義會在局部作用域中綁定模塊或函數名稱。

6. 小結

  1. 命名空間是一個名字(變量)和對象的映射表。
  2. 作用域是指命名空間的作用範圍,或者說管轄區域。
  3. 變量的查找遵循 LEGB 原則,先從基層(最內層函數找),然後到市委(外層函數)…,再到省委(模塊命名空間),最後到中央(builtin 命名空間)。
  4. 各個命名空間相互獨立,創建時間和生命週期各不相同。
  5. global 用於在函數內創建和修改全局變量。
  6. nonlocal 用於在內層函數修改外層函數局部變量。
  7. 沒有聲明 global 和 nonlocal,嘗試修改全局變量或外層函數局部變量,實際上只會在函數或者內層函數創建一個新的局部變量,同名的全局變量或者外層函數局部變量不會受影響。

7. 巨人的肩膀

  1. Pytho 官網作用域和命名空間英文版
  2. The global statement and The nonlocal statement

推薦閱讀:

  1. 編程小白安裝Python開發環境及PyCharm的基本用法
  2. 一文了解Python基礎知識
  3. 一文了解Python數據結構
  4. 一文了解Python流程控制
  5. 一文了解Python函數
  6. 一文了解Python部分高級特性
  7. 一文了解Python的模塊和包

如果我的筆記對你有所幫助,歡迎關注我的微信公衆號,加我好友,一起學習進步!
在這裏插入圖片描述

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