名字空間:Python 變量的容身之所
楔子
在介紹棧楨的時(shí)候,我們看到了 3 個(gè)獨(dú)立的名字空間:f_locals、f_globals、f_builtins。名字空間對(duì) Python 來(lái)說(shuō)是一個(gè)非常重要的概念,虛擬機(jī)的運(yùn)行機(jī)制和名字空間有著非常緊密的聯(lián)系。并且在 Python 中,與名字空間這個(gè)概念緊密聯(lián)系在一起的還有名字、作用域這些概念,下面我們就來(lái)剖析這些概念是如何體現(xiàn)的。
變量只是一個(gè)名字
在這個(gè)系列的最開始我們就說(shuō)過(guò),從解釋器的角度來(lái)看,變量只是一個(gè)泛型指針 PyObject *,而從 Python 的角度來(lái)看,變量只是一個(gè)名字、或者說(shuō)符號(hào),用于和對(duì)象進(jìn)行綁定的。
name = "古明地覺"
上面這個(gè)賦值語(yǔ)句其實(shí)就是將 name 和 "古明地覺" 綁定起來(lái),讓我們可以通過(guò) name 這個(gè)符號(hào)找到對(duì)應(yīng)的 PyUnicodeObject。因此定義一個(gè)變量,本質(zhì)上就是建立名字和對(duì)象之間的映射關(guān)系。
另外我們說(shuō) Python 雖然一切皆對(duì)象,但拿到的都是指向?qū)ο蟮闹羔槪虼藙?chuàng)建函數(shù)和類,以及模塊導(dǎo)入,同樣是在完成名字和對(duì)象的綁定。
def foo(): pass
class A(): pass
創(chuàng)建一個(gè)函數(shù)也相當(dāng)于定義一個(gè)變量,會(huì)先根據(jù)函數(shù)體創(chuàng)建一個(gè)函數(shù)對(duì)象,然后將名字 foo 和函數(shù)對(duì)象綁定起來(lái)。所以函數(shù)名和函數(shù)體之間是分離的,同理類也是如此。
import os
導(dǎo)入一個(gè)模塊,也是在定義一個(gè)變量。import os 相當(dāng)于將名字 os 和模塊對(duì)象綁定起來(lái),通過(guò) os 可以找到指定的模塊對(duì)象。
當(dāng)我們導(dǎo)入一個(gè)模塊的時(shí)候,解釋器是這么做的。
import os 等價(jià)于 os = __import__("os"),可以看到本質(zhì)上還是一個(gè)賦值語(yǔ)句。
import numpy as np 中的 as 語(yǔ)句同樣是在定義變量,將名字 np 和對(duì)應(yīng)的模塊對(duì)象綁定起來(lái),以后就可以通過(guò) np 這個(gè)名字去獲取指定的模塊了。
總結(jié):無(wú)論是普通的賦值語(yǔ)句,還是定義函數(shù)和類,亦或是模塊導(dǎo)入,它們本質(zhì)上都是在完成變量和對(duì)象的綁定。
name = "古明地覺"
def foo(): pass
class A(): pass
import os
import numpy as np
里面的 name、foo、A、os、np,都只是一個(gè)變量,或者說(shuō)名字、符號(hào),然后通過(guò)名字可以獲取與之綁定的對(duì)象。
作用域和名字空間
正如上面所說(shuō),賦值語(yǔ)句、函數(shù)定義、類定義、模塊導(dǎo)入,本質(zhì)上只是完成了變量和對(duì)象之間的綁定,或者說(shuō)我們創(chuàng)建了變量到對(duì)象的映射,通過(guò)變量可以獲取對(duì)應(yīng)的對(duì)象,而它們的容身之所就是名字空間。
所以名字空間是通過(guò) PyDictObject 對(duì)象實(shí)現(xiàn)的,這對(duì)于映射來(lái)說(shuō)簡(jiǎn)直再適合不過(guò)了。而前面介紹字典的時(shí)候,我們說(shuō)字典是被高度優(yōu)化的,原因就是虛擬機(jī)本身也重度依賴字典,從這里的名字空間即可得到體現(xiàn)。
當(dāng)然,在一個(gè)模塊內(nèi)部,變量還存在可見性的問(wèn)題,比如:
x = 1
def foo():
x = 2
print(x) # 2
foo()
print(x) # 1
我們看到同一個(gè)變量名,打印的確是不同的值,說(shuō)明指向了不同的對(duì)象,換句話說(shuō)這兩個(gè)變量是在不同的名字空間中被創(chuàng)建的。
名字空間本質(zhì)上是一個(gè)字典,如果兩者在同一個(gè)名字空間,那么由于 key 的不重復(fù)性,當(dāng)執(zhí)行 x = 2 的時(shí)候,會(huì)把字典里面 key 為 "x" 的 value 給更新成 2。但是在外面還是打印 1,這說(shuō)明兩者所在的不是同一個(gè)名字空間,打印的也就自然不是同一個(gè) x。
因此對(duì)于一個(gè)模塊而言,內(nèi)部可以存在多個(gè)名字空間,每一個(gè)名字空間都與一個(gè)作用域相對(duì)應(yīng)。作用域可以理解為一段程序的正文區(qū)域,在這個(gè)區(qū)域里面定義的變量是有意義的,然而一旦出了這個(gè)區(qū)域,就無(wú)效了。
關(guān)于作用域這個(gè)概念,我們要記住:它僅僅是由源代碼的文本所決定。在 Python 中,一個(gè)變量在某個(gè)位置是否起作用,是由它的文本位置決定的。
因此 Python 具有靜態(tài)作用域(詞法作用域),而名字空間則是作用域的動(dòng)態(tài)體現(xiàn),一個(gè)由程序文本定義的作用域在運(yùn)行時(shí)會(huì)轉(zhuǎn)化為一個(gè)名字空間、即一個(gè) PyDictObject 對(duì)象。比如進(jìn)入一個(gè)函數(shù),顯然會(huì)進(jìn)入一個(gè)新的作用域,因此函數(shù)在執(zhí)行時(shí),會(huì)創(chuàng)建一個(gè)名字空間。
在介紹 PyCodeObject 的時(shí)候,我們說(shuō)解釋器在對(duì)源代碼進(jìn)行編譯的時(shí)候,對(duì)于代碼中的每一個(gè) code block,都會(huì)創(chuàng)建一個(gè) PyCodeObject 對(duì)象與之對(duì)應(yīng)。而當(dāng)進(jìn)入一個(gè)新的名字空間、或者說(shuō)作用域時(shí),我們就算是進(jìn)入一個(gè)新的 block 了。
而根據(jù)我們使用 Python 的經(jīng)驗(yàn),顯然函數(shù)、類都是一個(gè)新的 block,解釋器在執(zhí)行的時(shí)候會(huì)為它們創(chuàng)建各自的名字空間。
所以名字空間是名字、或者說(shuō)變量的上下文環(huán)境,名字的含義取決于名字空間。更具體的說(shuō),一個(gè)變量綁定的對(duì)象是不確定的,需要由名字空間來(lái)決定。
位于同一個(gè)作用域的代碼可以直接訪問(wèn)作用域中出現(xiàn)的名字,即所謂的直接訪問(wèn);但不同的作用域,則需要通過(guò)訪問(wèn)修飾符 . 進(jìn)行屬性訪問(wèn)。
class A:
x = 1
class B:
y = 2
print(A.x) # 1
print(y) # 2
如果想在 B 里面訪問(wèn) A 里面的內(nèi)容,要通過(guò) A.屬性的方式,表示通過(guò) A 來(lái)獲取 A 里面的屬性。但是訪問(wèn) B 的內(nèi)容就不需要了,因?yàn)槎际窃谕粋€(gè)作用域,所以直接訪問(wèn)即可。
訪問(wèn)名字這樣的行為被稱為名字引用,名字引用的規(guī)則決定了 Python 程序的行為。
x = 1
def foo():
x = 2
print(x) # 2
foo()
print(x) # 1
還是上面的代碼,如果我們把函數(shù)里面的 a = 2 給刪掉,意味著函數(shù)的作用域里面已經(jīng)沒有 a 這個(gè)變量了,那么再執(zhí)行程序會(huì)有什么后果呢?從 Python 層面來(lái)看,顯然是會(huì)尋找外部的 a。因此我們可以得到如下結(jié)論:
- 作用域是層層嵌套的;
- 內(nèi)層作用域可以訪問(wèn)外層作用域;
- 外層作用域無(wú)法訪問(wèn)內(nèi)層作用域,如果是把外層的 a = 1 給去掉,那么最后面的 print(a) 鐵定報(bào)錯(cuò);
- 查找元素會(huì)依次從當(dāng)前作用域向外查找,也就是查找元素時(shí),對(duì)應(yīng)的作用域是按照從小往大、從里往外的方向前進(jìn)的;
global 名字空間
不光函數(shù)、類有自己的作用域,模塊對(duì)應(yīng)的源文件本身也有相應(yīng)的作用域。比如:
name = "古明地覺"
age = 16
def foo():
return 123
class A:
pass
這個(gè)文件本身也有自己的作用域,并且是 global 作用域,所以解釋器在運(yùn)行這個(gè)文件的時(shí)候,也會(huì)為其創(chuàng)建一個(gè)名字空間,而這個(gè)名字空間就是 global 名字空間,即全局名字空間。它里面的變量是全局的,或者說(shuō)是模塊級(jí)別的,在當(dāng)前文件的任意位置都可以直接訪問(wèn)。
而 Python 也提供了 globals 函數(shù),用于獲取 global 名字空間。
name = "古明地覺"
def foo():
pass
print(globals())
"""
{..., 'name': '古明地覺', 'foo': <function foo at 0x0000015255143E20>}
"""
里面的 ... 表示省略了一部分輸出,我們看到創(chuàng)建的全局變量就在里面。而且 foo 也是一個(gè)變量,它指向一個(gè)函數(shù)對(duì)象。
注意:我們說(shuō)函數(shù)內(nèi)部是一個(gè)獨(dú)立的 block,因此它會(huì)對(duì)應(yīng)一個(gè) PyCodeObject。然后在解釋到 def foo 的時(shí)候,會(huì)根據(jù) PyCodeObject 對(duì)象創(chuàng)建一個(gè) PyFunctionObject 對(duì)象,然后將 foo 和這個(gè)函數(shù)對(duì)象綁定起來(lái)。
當(dāng)我們調(diào)用 foo 的時(shí)候,再根據(jù) PyFunctionObject 對(duì)象創(chuàng)建 PyFrameObject 對(duì)象、然后執(zhí)行,至于具體細(xì)節(jié)留到介紹函數(shù)的時(shí)候再細(xì)說(shuō)??傊覀兛吹?foo 也是一個(gè)全局變量,全局變量都在 global 名字空間中。
總之,global 名字空間全局唯一,它是程序運(yùn)行時(shí)的全局變量和與之綁定的對(duì)象的容身之所。你在任何一個(gè)位置都可以訪問(wèn)到 global 名字空間,正如你在任何一個(gè)位置都可以訪問(wèn)全局變量一樣。
另外我們思考一下,global 名字空間是一個(gè)字典,全局變量和對(duì)象會(huì)以鍵值對(duì)的形式存在里面。那如果我手動(dòng)地往 global 名字空間里面添加一個(gè)鍵值對(duì),是不是也等價(jià)于定義一個(gè)全局變量呢?
globals()["name"] = "古明地覺"
print(name) # 古明地覺
def foo1():
def foo2():
def foo3():
globals()["age"] = 16
return foo3
return foo2
foo1()()()
print(age) # 16
我們看到確實(shí)如此,往 global 名字空間里面插入一個(gè)鍵值對(duì)完全等價(jià)于定義一個(gè)全局變量。并且 global 名字空間是唯一的,你在任何地方調(diào)用 globals() 得到的都是 global 名字空間,正如你在任何地方都可以訪問(wèn)到全局變量一樣。
所以即使是在函數(shù)中給 global 名字空間添加一個(gè)鍵值對(duì),也等價(jià)于定義一個(gè)全局變量。
圖片
問(wèn)題來(lái)了,如果在函數(shù)里面,我們不獲取 global 名字空間,怎么創(chuàng)建全局變量呢?
name = "古明地覺"
def foo():
global name
name = "古明地戀"
print(name) # 古明地覺
foo()
print(name) # 古明地戀
很簡(jiǎn)單,Python 為我們準(zhǔn)備了 global 關(guān)鍵字,表示聲明的變量是全局的。
local 名字空間
像函數(shù)和類擁有的作用域,我們稱之為 local 作用域,在運(yùn)行時(shí)會(huì)對(duì)應(yīng) local 名字空間,即局部名字空間。由于不同的函數(shù)具有不同的作用域,所以局部名字空間可以有很多個(gè),但全局名字空間只有一個(gè)。
對(duì)于 local 名字空間來(lái)說(shuō),它也對(duì)應(yīng)一個(gè)字典,顯然這個(gè)字典就不是全局唯一的了。而如果想獲取局部名字空間,Python 也提供了 locals 函數(shù)。
def foo():
name = "古明地覺"
age = 17
return locals()
def bar():
name = "霧雨魔理沙"
age = 18
return locals()
print(locals() == globals()) # True
print(foo()) # {'name': '古明地覺', 'age': 17}
print(bar()) # {'name': '霧雨魔理沙', 'age': 18}
顯然對(duì)于模塊來(lái)講,它的 local 名字空間和 global 名字空間是一樣的,也就是說(shuō),模塊對(duì)應(yīng)的棧楨對(duì)象里面的 f_locals 和 f_globals 指向的是同一個(gè) PyDictObject 對(duì)象。
但對(duì)于函數(shù)而言,局部名字空間和全局名字空間就不一樣了。調(diào)用 locals() 是獲取自身的局部名字空間,而不同函數(shù)的局部名字空間是不同的。但是 globals() 函數(shù)的調(diào)用結(jié)果是一樣的,獲取的都是全局名字空間,這也符合函數(shù)內(nèi)不存在指定變量的時(shí)候會(huì)去找全局變量這一結(jié)論。
注:關(guān)于 local 名字空間,還有一個(gè)重要的細(xì)節(jié),全局變量會(huì)存儲(chǔ)在 global 名字空間中,但局部變量卻并不存儲(chǔ)在 local 名字空間中。函數(shù)有哪些局部變量在編譯的時(shí)候就已經(jīng)確定了,會(huì)被靜態(tài)存儲(chǔ)在數(shù)組中,關(guān)于這一點(diǎn),后續(xù)會(huì)單獨(dú)詳細(xì)說(shuō)明。
builtin 名字空間
Python 有一個(gè)所謂的 LGB 規(guī)則,指的是在查找一個(gè)變量時(shí),會(huì)按照自身的 local 空間、外層的 global 空間、內(nèi)置的 builtin 空間的順序進(jìn)行查找。
builtin 名字空間也是一個(gè)字典,當(dāng) local 名字空間、global 名字空間都查找不到指定變量的時(shí)候,會(huì)去 builtin 空間查找。而關(guān)于 builtin 空間的獲取,Python 提供了一個(gè)模塊。
# 等價(jià)于 __builtins__
import builtins
print(builtins is __builtins__) # True
print(builtins) # <module 'builtins' (built-in)>
builtins 是一個(gè)模塊,那么 builtins.__dict__ 便是 builtin 名字空間,也叫內(nèi)置名字空間。
import builtins
# builtins.list 表示從 builtin 名字空間中查找 list
# 它等價(jià)于 builtins.__dict__["list"]
# 而如果只寫 list,那么由于 local 空間、global 空間都沒有
# 因此最終還是會(huì)從 builtin 空間中查找
# 但如果是 builtins.list,那么就不兜圈子了
# 表示:"builtin 空間,就從你這里獲取了"
print(builtins.list is list) # True
# 將 builtin 空間的 dict 改成 123
builtins.dict = 123
# 那么此時(shí)獲取的 dict 就是 123
print(dict + 456) # 579
# 如果是 str = 123,等價(jià)于創(chuàng)建全局變量 str = 123
str = 123
# 顯然影響的是 global 空間
print(str) # 123
# builtin 空間則不受影響
print(builtins.str) # <class 'str'>
print(builtins.__dict__["str"]) # <class 'str'>
這里提一下在 Python2 中,while 1 比 while True 要快,為什么?
因?yàn)?True 在 Python2 中不是關(guān)鍵字,所以它是可以作為變量名的。那么虛擬機(jī)在執(zhí)行的時(shí)候就要先看 local 空間和 global 空間里有沒有 True 這個(gè)變量,有的話使用我們定義的,沒有的話再使用內(nèi)置的 True。
而 1 是一個(gè)常量,直接加載就可以,所以 while True 多了符號(hào)查找這一過(guò)程。但是在 Python3 中兩者就等價(jià)了,因?yàn)?True 在 Python3 中是一個(gè)關(guān)鍵字,也會(huì)直接作為一個(gè)常量來(lái)加載。
exec 和 eval
記得之前介紹 exec 和 eval 的時(shí)候,我們說(shuō)這兩個(gè)函數(shù)里面還可以接收第二個(gè)參數(shù)和第三個(gè)參數(shù),它們分別表示 global 名字空間、local 名字空間。
# 如果不指定,默認(rèn)是當(dāng)前所在的名字空間
# 顯然此時(shí)是全局名字空間
exec("name = '古明地覺'")
print(name) # 古明地覺
# 但我們也可以指定某個(gè)名字空間
namespace = {}
# 比如將 namespace 作為全局名字空間
# 這里我們沒有指定第三個(gè)參數(shù),也就是局部名字空間
# 如果指定了第二個(gè)參數(shù),但沒有指定第三個(gè)參數(shù)
# 那么第三個(gè)參數(shù)默認(rèn)和第二個(gè)參數(shù)保持一致
exec("name = 'satori'", namespace)
print(namespace["name"]) # satori
至于 eval 也是同理:
namespace = {"seq": [1, 2, 3, 4, 5]}
try:
print(eval("sum(seq)"))
except NameError as e:
print(e) # name 'seq' is not defined
# 告訴我們 seq 沒有被定義
# 這里將 namespace 作為名字空間
print(eval("sum(seq)", namespace)) # 15
所以名字空間本質(zhì)上就是一個(gè)字典,所謂的變量不過(guò)是字典里面的一個(gè) key。為了進(jìn)一步加深印象,再舉個(gè)模塊的例子:
# 我們自定義一個(gè)模塊吧
# 首先模塊也是一個(gè)對(duì)象,類型為 <class 'module'>
# 但底層沒有將這個(gè)類暴露給我們,所以需要換一種方式獲取
import sys
ModuleType = type(sys)
# 以上就拿到了模塊的類型對(duì)象,調(diào)用即可得到模塊對(duì)象
# 這里我們自定義一個(gè)類,繼承 ModuleType
class MyModule(ModuleType):
def __init__(self, module_name):
self.module_name = module_name
super().__init__(module_name)
# 也可以定義一些其它的屬性
def __str__(self):
return f"<module '{self.module_name}' from '虛無(wú)之境'>"
my_module = MyModule("自定義模塊")
print(my_module)
"""
<module '自定義模塊' from '虛無(wú)之境'>
"""
# 此時(shí)的 my_module 啥也沒有,我們?yōu)槠涮泶u加瓦
my_module.__dict__["name"] = "古明地覺"
print(my_module.name) # 古明地覺
# 給模塊設(shè)置屬性,本質(zhì)上也是操作模塊的屬性字典,當(dāng)然獲取屬性也是如此
# 如果再和 exec 結(jié)合的話
code_string = """
age = 16
def foo():
return "我是函數(shù) foo"
from functools import reduce
"""
# 此時(shí)屬性就設(shè)置在了模塊的屬性字典里面
exec(code_string, my_module.__dict__)
# 然后我們獲取它
print(my_module.age) # 16
print(my_module.foo()) # 我是函數(shù) foo
print(my_module.reduce(int.__add__, range(101))) # 5050
# 是不是很神奇呢?由于 my_module 是一個(gè)模塊對(duì)象
# 我們還可以將它注入到 sys.modules 中,然后通過(guò) import 獲取
sys.modules["俺滴模塊"] = my_module
from 俺滴模塊 import name, age, foo
print(name) # 古明地覺
print(age) # 16
print(foo()) # 我是函數(shù) foo
怎么樣,是不是很有意思呢?相信你對(duì)名字空間已經(jīng)有了足夠清晰的認(rèn)識(shí),它是變量和與之綁定的對(duì)象的容身之所。
小結(jié)
名字空間是 Python 的靈魂,它規(guī)定了一個(gè)變量應(yīng)該如何查找,關(guān)于變量查找,到時(shí)你會(huì)對(duì)名字空間有更加透徹的理解。
然后是作用域,所謂名字空間其實(shí)就是作用域的動(dòng)態(tài)體現(xiàn)。整個(gè) py 文件是一個(gè)作用域,也是全局作用域;定義函數(shù)、定義類、定義方法,又會(huì)創(chuàng)建新的作用域,這些作用域?qū)訉忧短住?/p>
那么同理,運(yùn)行時(shí)的名字空間也是層層嵌套的,形成一條名字空間鏈。內(nèi)層的變量對(duì)于外層是不可見的,但外層的變量對(duì)內(nèi)層是可見的。
然后全局名字空間是一個(gè)字典,它是唯一的,操作里面的鍵值對(duì)等價(jià)于操作全局變量;至于局部名字空間則不唯一,每一個(gè)函數(shù)都有自己的局部名字空間,但我們要知道函數(shù)內(nèi)部在訪問(wèn)局部變量的時(shí)候是靜態(tài)訪問(wèn)的(相關(guān)細(xì)節(jié)后續(xù)聊)。
還有內(nèi)置名字空間,可以通過(guò) __builtins__ 獲取,但拿到的是一個(gè)模塊,再獲取它的屬性字典,那么就是內(nèi)置名字空間了。