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

學無先后,達者為師

網站首頁 編程語言 正文

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

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

楔子

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

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

變量只是一個符號

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

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

除了變量賦值,創建函數、類也相當于定義變量,或者說完成名字和對象之間的綁定。

def?foo():?pass

class?A():?pass

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

import?os

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

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

另外,當我們導入一個模塊的時候,解釋器是這么做的,比如 import os

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

變量的可見性

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

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

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

number?=?123

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

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

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

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

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

對于作用域這個概念,至關重要的是要記住:它僅僅是由程序的文本所決定的。在 Python 中,一個變量在某個位置是否起作用,是由它的文本位置決定的。

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

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

而根據我們使用 Python 的經驗,顯然函數、類都是一個新的 block,當 Python 運行的時候會為它們創建各自的名字空間。

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

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

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


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

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

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

number?=?123

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

foo()
print(number)??

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

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

LGB 規則

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

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

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

class?A:
????pass

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

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

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

而獲取名字空間,Python也提供了相應的內置函數:

  • locals 函數:獲取當前作用域的 local 名字空間,local 名字空間也稱為局部名字空間;
  • globals 函數:獲取當前作用域的 global 名字空間,global 名字空間也稱為全局名字空間;
  • __builtins__函數:或者 import builtins,獲取當前作用域的 builtin 名字空間,builtint 名字空間也稱為內置名字空間;

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

name?=?"古明地覺"

def?foo():
????pass

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

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

當調用 foo 的時候,再根據 PyFunctionObject 對象創建 PyFrameObject 對象、然后執行,這些留在介紹函數的時候再細說。但是我們看到 foo 也是一個全局變量,全局變量都在 global 名字空間中。

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

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

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

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


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

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

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

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

對于?local 名字空間來說,它也對應一個字典,顯然這個字典就不是全局唯一的了,每一個局部作用域都會對應自身的 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名字空間是一樣的,也就是說,模塊對應的 PyFrameObject 對象里面的 f_locals 和 f_globals 指向的是同一個 PyDictObject 對象。

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

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

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

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

#?等價于__builtins__
import?builtins

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

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

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

這里提一下Python2當中,while 1比while True要快,為什么?

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

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

局部變量是靜態存儲的

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

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

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

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

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

所以,雖然我們說遍歷查找是按照 LGB 的方式,但是訪問函數內部的變量其實是靜態訪問的,不過完全可以按照 LGB 的方式理解。關于這方面的細節,后續還會細說。

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

LEGB 規則

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

a?=?1

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

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

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

調用 f,實際上調用的是函數 bar,最終輸出的結果是 2。如果按照 LGB 的規則來查找的話,由于函數 bar 的作用域沒有 a,那么應該到全局里面找,打印的結果是 1 才對。

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

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

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

所以:閉包 = 內層函數 + 引用的外層作用域的變量

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

global 表達式

有一個很奇怪的問題,最開始學習 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,結果就報錯了,提示局部變量 a 在賦值之前就被引用了,這是怎么一回事,相信肯定有人為此困惑。而想弄明白這個錯誤的原因,需要深刻理解兩點:

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

函數中的變量是靜態存儲、靜態訪問的, 內部有哪些變量在編譯的時候就已經確定;

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

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

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
"""

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

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

當然 Python 也為我們精心準備了 global 關鍵字,讓我們在函數內部修改全局變量。比如函數內部出現了?global a,就表示我后面的 a 是全局的,直接到 global 名字空間里面去找,不要在 local 空間里面找了。

a?=?1

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

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

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

a?=?1

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

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

屬性空間

我們知道,自定義的類里面如果沒有 __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"])??#?古明地覺?古明地覺

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

當然模塊也有屬性字典,本質上和普通的類的實例對象是一致的。

import?builtins

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

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

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

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 名字空間,都保存了指向彼此的指針,不管套娃多少次,都是可以的。

小結

名字空間是 Python 的靈魂,它規定了一個變量應該如何查找,而名字空間是作用域的動態體現。整個 py 文件是一個作用域,也是全局作用域;定義函數、定義類、定義方法,又會創建新的作用域,這些作用域層層嵌套。

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

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

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

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

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

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