日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達者為師

網(wǎng)站首頁 編程語言 正文

解密Python中的作用域與名字空間_python

作者:古明地覺 ? 更新時間: 2023-07-02 編程語言

楔子

前面我們介紹了棧幀,在里面看到了 3 個獨立的名字空間:f_locals、f_globals、f_builtins。名字空間對于 Python 來說是一個非常重要的概念,虛擬機的運行機制和名字空間有著非常緊密的聯(lián)系。

并且與名字空間這個概念緊密聯(lián)系在一起的還有名字、作用域這些概念,下面就來剖析這些概念是如何體現(xiàn)的。

變量只是一個符號

從解釋器的角度來看,變量只是一個泛型指針 PyObject *;而從 Python 的角度來看,變量只是一個用來和對象進行綁定的名字、或者說符號。

變量的定義本質(zhì)上就是建立名字和對象之間的約束關(guān)系,所以?a = 1?這個賦值語句本質(zhì)上就是將 a??1?綁定起來,讓我們通過 a 這個符號可以找到對應(yīng)的 PyLongObject。

除了變量賦值,創(chuàng)建函數(shù)、類也相當(dāng)于定義變量,或者說完成名字和對象之間的綁定。

def?foo():?pass

class?A():?pass

創(chuàng)建一個函數(shù)也相當(dāng)于定義一個變量,會先根據(jù)函數(shù)體創(chuàng)建一個函數(shù)對象,然后將符號?foo?和函數(shù)對象綁定起來。所以函數(shù)名和函數(shù)體之間是分離的,同理類也是如此。

import?os

導(dǎo)入一個模塊,也是在定義一個變量。import os?相當(dāng)于將名字 os 和模塊對象綁定起來,通過 os 可以找到指定的模塊對象。

再比如?import numpy as np?當(dāng)中的 as 語句同樣是在定義變量,將名字 np 和對應(yīng)的模塊對象綁定起來,以后就可以通過 np 這個名字去獲取指定的模塊了。

另外,當(dāng)我們導(dǎo)入一個模塊的時候,解釋器是這么做的,比如 import os

它等價于 os = __import__("os"),可以看到本質(zhì)上還是一個變量賦值語句。

變量的可見性

我們知道賦值語句、函數(shù)定義、類定義、模塊導(dǎo)入,本質(zhì)上只是完成了名字和對象之間的綁定。而從概念上講,我們實際上就是得到了一個 name 和 obj 之間的映射關(guān)系,通過 name 可以獲取對應(yīng)的 obj,而它們的容身之所就是名字空間。

那么要實現(xiàn)名字空間,應(yīng)該選擇哪一種數(shù)據(jù)結(jié)構(gòu)呢?毫無疑問,肯定是字典。而在前面介紹字典的時候,我們說字典是被高度優(yōu)化的,原因就是虛擬機本身也在大量使用字典,從這里的名字空間即可得到體現(xiàn)。

但是一個模塊內(nèi)部,名字(變量)還存在可見性的問題,比如:

number?=?123

def?foo():
????number?=?456
????print(number)

foo()
print(number)??
"""
456
123
"""

我們看到同一個變量名,打印的確是不同的值,說明指向了不同的對象,換句話說這兩個變量是在不同的名字空間中被創(chuàng)建的。

因為名字空間是一個字典,如果兩者是在同一個名字空間,那么由于字典的 key 的不重復(fù)性,當(dāng)執(zhí)行 number = 456 的時候,會把字典里面 key 為 "number"?的 value 給更新成 456。但外面還是打印 123,這說明兩者所在的不是同一個名字空間,打印的也就自然不是同一個 number。

因此對于一個模塊而言,內(nèi)部是可能存在多個名字空間的,每一個名字空間都與一個作用域相對應(yīng)。作用域可以理解為一段程序的正文區(qū)域,在這個區(qū)域里面定義的變量是有意義的,然而一旦出了這個區(qū)域,就無效了。

對于作用域這個概念,至關(guān)重要的是要記?。核鼉H僅是由程序的文本所決定的。在 Python 中,一個變量在某個位置是否起作用,是由它的文本位置決定的。

因此 Python 具有靜態(tài)詞法作用域,而名字空間則是作用域的動態(tài)體現(xiàn),一個由程序文本定義的作用域在 Python 運行時會轉(zhuǎn)化為一個名字空間(字典)。比如進入一個函數(shù),顯然進入了一個新的作用域,因此函數(shù)在執(zhí)行時,會創(chuàng)建一個名字空間。

Python 在對源代碼進行編譯的時候,對于代碼中的每一個 block,都會創(chuàng)建一個PyCodeObject 與之對應(yīng)。而當(dāng)進入一個新的名字空間、或者說作用域時,我們就算是進入一個新的 block 了。

而根據(jù)我們使用 Python 的經(jīng)驗,顯然函數(shù)、類都是一個新的 block,當(dāng) Python 運行的時候會為它們創(chuàng)建各自的名字空間。

所以名字空間是名字(變量)的上下文環(huán)境,名字的含義取決于名字空間。更具體的說,一個變量綁定的對象是不確定的,需要由名字空間來決定。

位于同一個作用域的代碼可以直接訪問作用域中出現(xiàn)的名字,即所謂的直接訪問;但不同作用域,則需要通過訪問修飾符?.?進行屬性訪問。

class?A:
????a?=?1


class?B:
????b?=?2
????print(A.a)??#?1
????print(b)??#?2

如果想在 B 里面訪問 A 里面的內(nèi)容,要通過A.屬性的方式,表示通過 A 來獲取 A 里面的屬性。但是訪問 B 的內(nèi)容就不需要了,因為都是在同一個作用域,所以直接訪問即可。

訪問名字這樣的行為被稱為名字引用,名字引用的規(guī)則決定了 Python 程序的行為。

number?=?123

def?foo():
????number?=?456
????print(number)

foo()
print(number)??

還是上面的代碼,如果把函數(shù)里面的賦值語句給刪掉,那么再執(zhí)行程序會有什么后果呢?從 Python 層面來看,由于函數(shù)的作用域里面沒有 number?這個變量,顯然會尋找外部的 number。因此我們可以得到如下結(jié)論:

  • 作用域是層層嵌套的;
  • 內(nèi)層作用域可以訪問外層作用域;
  • 外層作用域無法訪問內(nèi)層作用域,盡管我們沒有試,但是想都不用想。如果是把外層的 number = 123 給去掉,那么最后面的 print(number) 鐵定報錯;
  • 查找元素會依次從當(dāng)前作用域向外查找,也就是查找元素時,對應(yīng)的作用域是按照從小往大、從里往外的方向前進的;

LGB 規(guī)則

我們說函數(shù)、類有自己的作用域,但是模塊對應(yīng)的源文件本身也有相應(yīng)的作用域。比如:

name?=?"古明地覺"
age?=?16

def?foo():
????return?123

class?A:
????pass

由于文件本身也有自己的作用域,顯然是 global 作用域,所以解釋器在運行這個文件的時候,也會為其創(chuàng)建一個名字空間,而這個名字空間就是 global 名字空間。它里面的變量是全局的,或者說是模塊級別的,在當(dāng)前文件的任意位置都可以直接訪問。

而函數(shù)也有作用域,這個作用域稱為 local 作用域,對應(yīng) local 名字空間;同時 Python 自身還定義了一個最頂層的作用域,也就是 builtin 作用域,像內(nèi)置函數(shù)、內(nèi)建對象都在 builtin 里面。

這三個作用域在Python2.2之前就存在了,所以那時候Python的作用域規(guī)則被稱之為LGB規(guī)則:名字引用動作沿著local作用域(local名字空間)、global作用域(global名字空間)、builtin作用域(builtin名字空間)來查找對應(yīng)的變量。

而獲取名字空間,Python也提供了相應(yīng)的內(nèi)置函數(shù):

  • locals 函數(shù):獲取當(dāng)前作用域的 local 名字空間,local 名字空間也稱為局部名字空間;
  • globals 函數(shù):獲取當(dāng)前作用域的 global 名字空間,global 名字空間也稱為全局名字空間;
  • __builtins__函數(shù):或者 import builtins,獲取當(dāng)前作用域的 builtin 名字空間,builtint 名字空間也稱為內(nèi)置名字空間;

每個函數(shù)都有自己 local 名字空間,因為不同的函數(shù)對應(yīng)不同的作用域,但是 global 名字空間則是全局唯一。

name?=?"古明地覺"

def?foo():
????pass

print("name"?in?globals())
print("foo"?in?globals())
"""
True
True
"""

但是注意,我們說 foo 也是一個獨立的 block,因此它會對應(yīng)一個 PyCodeObject。但是在解釋到?def foo?的時候,會根據(jù)這個 PyCodeObject 對象創(chuàng)建一個 PyFunctionObject對象,然后將字符串 foo 和這個函數(shù)對象綁定起來。

當(dāng)調(diào)用 foo 的時候,再根據(jù) PyFunctionObject 對象創(chuàng)建 PyFrameObject 對象、然后執(zhí)行,這些留在介紹函數(shù)的時候再細(xì)說。但是我們看到 foo 也是一個全局變量,全局變量都在 global 名字空間中。

總之,global名字空間全局唯一,它是程序運行時的全局變量和與之綁定的對象的容身之所,你在任何一個地方都可以訪問到 global 名字空間。正如,你在任何一個地方都可以訪問相應(yīng)的全局變量一樣。

此外,我們說名字空間是一個字典,變量和對象會以鍵值對的形式存在里面。那么換句話說,如果我手動地往這個 global 名字空間里面添加一個鍵值對,是不是也等價于定義一個全局變量呢?

globals()["name"]?=?"古明地覺"
print(name)??#?古明地覺

def?f1():
????def?f2():
????????def?f3():
????????????globals()["age"]?=?16
????????return?f3
????return?f2


f1()()()
print(age)??#?16

我們看到確實如此,通過往 global 名字空間里面插入一個鍵值對完全等價于定義一個全局變量。并且 global 名字空間是唯一的,你在任何地方調(diào)用 globals() 得到的都是 global 名字空間,正如你在任何地方都可以訪問到全局變量一樣。

所以即使是在函數(shù)中向 global 名字空間中插入一個鍵值對,也等價于定義一個全局變量、并和對象綁定起來。

  • name="xxx" 等價于 globals["name"]="xxx";
  • print(name) 等價于 print(globals["name"]);

對于?local 名字空間來說,它也對應(yīng)一個字典,顯然這個字典就不是全局唯一的了,每一個局部作用域都會對應(yīng)自身的 local 名字空間。

def?f():
????name?=?"古明地覺"
????age?=?16
????return?locals()


def?g():
????name?=?"古明地戀"
????age?=?15
????return?locals()


print(locals()?==?globals())??#?True
print(f())??#?{'name':?'古明地覺',?'age':?16}
print(g())??#?{'name':?'古明地戀',?'age':?15}

顯然對于模塊來講,它的local名字空間和global名字空間是一樣的,也就是說,模塊對應(yīng)的 PyFrameObject 對象里面的 f_locals 和 f_globals 指向的是同一個 PyDictObject 對象。

但是對于函數(shù)而言,局部名字空間和全局名字空間就不一樣了。調(diào)用 locals 是獲取自身的局部名字空間,而不同函數(shù)的 local 名字空間是不同的。但是 globals 函數(shù)的調(diào)用結(jié)果是一樣的,獲取的都是 global 名字空間,這也符合函數(shù)內(nèi)找不到某個變量的時候會去找全局變量這一結(jié)論。

所以我們說在函數(shù)里面查找一個變量,查找不到的話會找全局變量,全局變量再沒有會查找內(nèi)置變量。本質(zhì)上就是按照自身的 local 空間、外層的 global 空間、內(nèi)置的 builtin 空間的順序進行查找。

因此 local 空間會有很多個,因為每一個函數(shù)或者類都有自己的局部作用域,這個局部作用域就可以稱之為該函數(shù)的 local 空間;但是 global 空間則全局唯一,因為該字典存儲的是全局變量。無論你在什么地方,通過調(diào)用 globals 函數(shù)拿到的永遠是全局名字空間,向該空間中添加鍵值對,等價于創(chuàng)建全局變量。

對于 builtin 名字空間,它也是一個字典。當(dāng) local 空間、global 空間都沒有的時候,會去 builtin 空間查找。那么 builtin 名字空間如何獲取呢?答案是使用 builtins 模塊,通過 builtins.__dict__ 即可拿到 builtin 名字空間。

#?等價于__builtins__
import?builtins

#我們調(diào)用?list?顯然是從內(nèi)置作用域、也就是?builtin?名字空間中查找的
#但我們只寫?list?也是可以的
#因為?local?空間、global?空間沒有的話,最終會從?builtin?空間中查找
#但如果是?builtins.list,那么就不兜圈子了
#表示:?"builtin?空間,就從你這獲取了"
print(builtins.list?is?list)??#?True

builtins.dict?=?123
#將?builtin?空間的?dict?改成?123
#那么此時獲取的?dict?就是123
#因為是從內(nèi)置作用域中獲取的
print(dict?+?456)??#?579

str?=?123
#如果是?str?=?123,等價于創(chuàng)建全局變量?str?=?123
#顯然影響的是?global?空間
print(str)??#?123
#?但是此時不影響?builtin?空間
print(builtins.str)??#?<class?'str'>

這里提一下Python2當(dāng)中,while 1比while True要快,為什么?

因為 True 在 Python2 中不是關(guān)鍵字,所以它是可以作為變量名的。那么 Python 在執(zhí)行的時候就要先看 local 空間和 global 空間里有沒有 True 這個變量,有的話使用我們定義的,沒有的話再使用內(nèi)置的 True。

而 1 是一個常量,直接加載就可以,所以 while True 多了符號查找這一過程。但是在 Python3 中兩者就等價了,因為 True 在 Python3 中是一個關(guān)鍵字,也會直接作為一個常量來加載。

局部變量是靜態(tài)存儲的

我們往 global 空間添加一個鍵值對相當(dāng)于定義一個全局變量,那么如果往函數(shù)的 local 空間里面添加一個鍵值對,是不是也等價于創(chuàng)建了一個局部變量呢?

def?func():
????locals()["where"]?=?"地靈殿"
????try:
????????print(where)
????except?Exception?as?e:
????????print(e)

func()??#?name?'where'?is?not?defined

對于全局變量來講,變量的創(chuàng)建是通過向字典添加鍵值對的方式實現(xiàn)的。因為全局變量會一直在變,需要使用字典來動態(tài)維護。

但對于函數(shù)來講,內(nèi)部的變量是通過靜態(tài)方式存儲和訪問的,因為局部作用域中存在哪些變量在編譯的時候就已經(jīng)確定了,我們通過 PyCodeObject 的 co_varnames 即可獲取內(nèi)部都有哪些變量。

所以,雖然我們說遍歷查找是按照 LGB 的方式,但是訪問函數(shù)內(nèi)部的變量其實是靜態(tài)訪問的,不過完全可以按照 LGB 的方式理解。關(guān)于這方面的細(xì)節(jié),后續(xù)還會細(xì)說。

因此名字空間是 Python 的靈魂,它規(guī)定了 Python 變量的作用域,使得 Python 對變量的查找變得非常清晰。

LEGB 規(guī)則

前面說的 LGB 是針對 Python2.2 之前的,而從 Python2.2 開始,由于引入了嵌套函數(shù),所以最好的方式應(yīng)該是內(nèi)層函數(shù)找不到某個變量時先去外層函數(shù)找,而不是直接就跑到 global 空間里面找。那么此時的規(guī)則就是LEGB:

a?=?1

def?foo():
????a?=?2

????def?bar():
????????print(a)
????return?bar

f?=?foo()
f()
"""
2
"""

調(diào)用 f,實際上調(diào)用的是函數(shù) bar,最終輸出的結(jié)果是 2。如果按照 LGB 的規(guī)則來查找的話,由于函數(shù) bar 的作用域沒有 a,那么應(yīng)該到全局里面找,打印的結(jié)果是 1 才對。

但我們之前說了,作用域僅僅是由文本決定的,函數(shù) bar 位于函數(shù) foo 之內(nèi),所以函數(shù) bar 定義的作用域內(nèi)嵌于函數(shù) foo 的作用域之內(nèi)。換句話說,函數(shù) foo 的作用域是函數(shù) bar 的直接外圍作用域。

所以應(yīng)該先從 foo 的作用域里面找,如果沒有那么再去全局里面找。而作用域和名字空間是對應(yīng)的,名字空間是作用域的動態(tài)體現(xiàn),所以最終打印了 2。

另外在執(zhí)行?f = foo()?的時候,會執(zhí)行函數(shù) foo 中的?def bar():?語句,這個時候解釋器會將?a=2?與函數(shù) bar?捆綁在一起,然后返回,這個捆綁起來的整體就叫做閉包。

所以:閉包 = 內(nèi)層函數(shù) + 引用的外層作用域的變量

這里顯示的規(guī)則就是 LEGB,其中 E 表示 enclosing,代表直接外圍作用域。

global 表達式

有一個很奇怪的問題,最開始學(xué)習(xí) Python 的時候,估計很多人都為此感到困惑,下面來看一下。

a?=?1

def?foo():
????print(a)

foo()
"""
1
"""

首先這段代碼打印 1,這顯然是沒有問題的,不過下面問題來了。

a?=?1

def?foo():
????print(a)
????a?=?2

foo()
"""
UnboundLocalError:?local?variable?'a'?referenced?before?assignment
"""

僅僅是在 print 語句后面新建了一個變量 a,結(jié)果就報錯了,提示局部變量 a 在賦值之前就被引用了,這是怎么一回事,相信肯定有人為此困惑。而想弄明白這個錯誤的原因,需要深刻理解兩點:

一個賦值語句所定義的變量,在這個賦值語句所在的整個作用域內(nèi)都是可見的;

函數(shù)中的變量是靜態(tài)存儲、靜態(tài)訪問的, 內(nèi)部有哪些變量在編譯的時候就已經(jīng)確定;

在編譯的時候,因為存在?a = 2?這條語句,所以知道函數(shù)中存在一個局部變量 a,那么查找的時候就會在當(dāng)前作用域中查找。但是還沒來得及賦值,就 print(a) 了,所以報錯:局部變量a在賦值之前就被引用了。但如果沒有?a = 2?這條語句則不會報錯,因為知道局部作用域中不存在 a 這個變量,所以會找全局變量 a,從而打印 1。

更有趣的東西隱藏在字節(jié)碼當(dāng)中,我們可以通過反匯編來查看一下:

import?dis

a?=?1

def?g():
????print(a)
????
dis.dis(g)
"""
??7???????????0?LOAD_GLOBAL??????????????0?(print)
??????????????2?LOAD_GLOBAL??????????????1?(a)
??????????????4?CALL_FUNCTION????????????1
??????????????6?POP_TOP
??????????????8?LOAD_CONST???????????????0?(None)
?????????????10?RETURN_VALUE
"""

def?f():
????print(a)
????a?=?2

dis.dis(f)
"""
?12???????????0?LOAD_GLOBAL??????????????0?(print)
??????????????2?LOAD_FAST????????????????0?(a)
??????????????4?CALL_FUNCTION????????????1
??????????????6?POP_TOP

?13???????????8?LOAD_CONST???????????????1?(2)
?????????????10?STORE_FAST???????????????0?(a)
?????????????12?LOAD_CONST???????????????0?(None)
?????????????14?RETURN_VALUE
"""

中間的序號代表字節(jié)碼的偏移量,我們看第二條,g 的字節(jié)碼是 LOAD_GLOBAL,意思是在 global 名字空間中查找;而 f 的字節(jié)碼是LOAD_FAST,表示靜態(tài)查找。因此結(jié)果說明 Python 采用了靜態(tài)作用域策略,在編譯的時候就已經(jīng)知道了名字藏身于何處。

而且上面的例子也表明,一旦函數(shù)內(nèi)有了對某個名字的賦值操作,這個名字就會在作用域內(nèi)可見,就會出現(xiàn)在 local 名字空間中。換句話說,會遮蔽外層作用域中相同的名字。

當(dāng)然 Python 也為我們精心準(zhǔn)備了 global 關(guān)鍵字,讓我們在函數(shù)內(nèi)部修改全局變量。比如函數(shù)內(nèi)部出現(xiàn)了?global a,就表示我后面的 a 是全局的,直接到 global 名字空間里面去找,不要在 local 空間里面找了。

a?=?1

def?bar():
????def?foo():
????????global?a
????????a?=?2
????return?foo

bar()()
print(a)??#?2
#?當(dāng)然,也可以通過 globals 函數(shù)拿到名字空間
#?然后直接修改里面的鍵值對

但如果外層函數(shù)里面也出現(xiàn)了變量 a,而我們想修改的也是外層函數(shù)的 a、不是全局的 a,這時該怎么辦呢?Python 同樣為我們準(zhǔn)備了關(guān)鍵字 nonlocal,但是使用 nonlocal 的時候,必須是在內(nèi)層函數(shù)里面。

a?=?1

def?bar():
????a?=?2
????def?foo():
????????nonlocal?a
????????a?=?"xxx"
????return?foo

bar()()
print(a)??#?1
#?外界依舊是 1,但是 bar 里面的 a 已經(jīng)被修改了

屬性空間

我們知道,自定義的類里面如果沒有 __slots__,那么這個類的實例對象都會有一個屬性字典。

class?Girl:

????def?__init__(self):
????????self.name?=?"古明地覺"
????????self.age?=?16


g?=?Girl()
print(g.__dict__)??#?{'name':?'古明地覺',?'age':?16}

#?對于查找屬性而言,?也是去屬性字典中查找
print(g.name,?g.__dict__["name"])??#?古明地覺?古明地覺

#?同理設(shè)置屬性,?也是更改對應(yīng)的屬性字典
g.__dict__["gender"]?=?"female"
print(g.gender)??#?female

當(dāng)然模塊也有屬性字典,本質(zhì)上和普通的類的實例對象是一致的。

import?builtins

print(builtins.str)??#?<class?'str'>
print(builtins.__dict__["str"])??#?<class?'str'>

#?另外,有一個內(nèi)置的變量?__builtins__,和導(dǎo)入的?builtins?等價
print(__builtins__?is?builtins)??#?True

另外這個?__builtins__?位于?global 名字空間里面,然后獲取 global 名字空間的?globals?又是一個內(nèi)置函數(shù),于是一個神奇的事情就出現(xiàn)了。

print(globals()["__builtins__"].globals()["__builtins__"].
??????globals()["__builtins__"].globals()["__builtins__"].
??????globals()["__builtins__"].globals()["__builtins__"]
??????)??#?<module?'builtins'?(built-in)>

print(globals()["__builtins__"].globals()["__builtins__"].
??????globals()["__builtins__"].globals()["__builtins__"].
??????globals()["__builtins__"].globals()["__builtins__"].list("abc")
??????)??#?['a',?'b',?'c']

所以 global 名字空間和 builtin 名字空間,都保存了指向彼此的指針,不管套娃多少次,都是可以的。

小結(jié)

名字空間是 Python 的靈魂,它規(guī)定了一個變量應(yīng)該如何查找,而名字空間是作用域的動態(tài)體現(xiàn)。整個 py 文件是一個作用域,也是全局作用域;定義函數(shù)、定義類、定義方法,又會創(chuàng)建新的作用域,這些作用域?qū)訉忧短住?/p>

那么同理,運行時的名字空間也是層層嵌套的,形成一條名字空間鏈。內(nèi)層的變量對于外層是不可見的,但外層的變量對內(nèi)層是可見的。

然后全局名字空間是一個字典,它是唯一的,操作里面的鍵值對等價于操作全局變量;

至于局部名字空間則不唯一,每一個函數(shù)都有自己的局部名字空間,但我們要知道函數(shù)內(nèi)部在訪問變量的時候是靜態(tài)訪問的。

還有內(nèi)置名字空間,可以通過 __builtins__ 獲取,但拿到的是一個模塊,再獲取它的屬性字典,那么就是內(nèi)置名字空間了。

原文鏈接:https://mp.weixin.qq.com/s/DpQ88gNuIH9Qqu8l-DeYqg

  • 上一篇:沒有了
  • 下一篇:沒有了
欄目分類
最近更新