自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

當(dāng)查找一個(gè) Python 變量時(shí),虛擬機(jī)會(huì)進(jìn)行哪些動(dòng)作?

開發(fā) 前端
整個(gè)內(nèi)容很好理解,關(guān)鍵的地方就在于局部變量,它是靜態(tài)存儲(chǔ)的,編譯期間就已經(jīng)確定。而在訪問局部變量時(shí),也是基于數(shù)組實(shí)現(xiàn)的靜態(tài)查找,而不是使用字典。

楔子

上一篇文章我們介紹了名字空間,并且知道了全局變量都存在 global 名字空間中,往 global 空間添加一個(gè)鍵值對(duì)相當(dāng)于定義一個(gè)全局變量。那么問題來了,如果往函數(shù)的 local 空間里面添加一個(gè)鍵值對(duì),是不是也等價(jià)于創(chuàng)建了一個(gè)局部變量呢?

def f1():
    locals()["name "] = "古明地覺"
    try:
        print(name)
    except Exception as e:
        print(e)

f1()  # name 'name' is not defined

全局變量的創(chuàng)建是通過向字典添加鍵值對(duì)實(shí)現(xiàn)的,因?yàn)槿肿兞繒?huì)一直變,需要使用字典來動(dòng)態(tài)維護(hù)。

但對(duì)于函數(shù)來講,內(nèi)部的變量是通過靜態(tài)方式存儲(chǔ)和訪問的,因?yàn)榫植孔饔糜蛑写嬖谀男┳兞吭诰幾g的時(shí)候就已經(jīng)確定了,我們通過 PyCodeObject 的 co_varnames 即可獲取內(nèi)部都有哪些變量。

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

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

LEGB 規(guī)則

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

x = 1

def foo():
    x = 2
    def bar():
        print(x)
    return bar

foo()()
"""
2
"""

調(diào)用了內(nèi)層函數(shù) bar,如果按照 LGB 的規(guī)則來查找的話,由于函數(shù) bar 的作用域沒有 a,那么應(yīng)該到全局里面找,打印的結(jié)果是 1 才對(duì)。

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

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

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

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

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

global 表達(dá)式

在初學(xué) Python 時(shí),估計(jì)很多人都會(huì)對(duì)下面的問題感到困惑。

x = 1

def foo():
    print(x)

foo()
"""
1
"""

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

x = 1

def foo():
    print(x)
    x = 2

foo()

這段代碼在執(zhí)行 print(x) 的時(shí)候是會(huì)報(bào)錯(cuò)的,會(huì)拋出一個(gè) UnboundLocalError。

圖片圖片

意思就是說,無法訪問局部變量 x,因?yàn)樗€沒有和某個(gè)值(對(duì)象)進(jìn)行綁定。當(dāng)然,如果是以前的 Python 版本,比如 3.8,同樣會(huì)拋出這個(gè)錯(cuò)誤,只是信息不同。

圖片圖片

意思是局部變量 x 在賦值之前就被使用了,所以盡管報(bào)錯(cuò)信息不同,但表達(dá)的含義是一樣的。

那么問題來了,在 print(x) 的下面加一個(gè) x = 2,整體效果不應(yīng)該是先打印全局變量 x,然后再創(chuàng)建一個(gè)局部變量 x 嗎?為啥就報(bào)錯(cuò)了呢,相信肯定有人為此困惑。如果想弄明白這個(gè)錯(cuò)誤的原因,需要深刻理解兩點(diǎn):

  • 函數(shù)中的變量是靜態(tài)存儲(chǔ)、靜態(tài)訪問的, 內(nèi)部有哪些變量在編譯的時(shí)候就已經(jīng)確定;
  • 局部變量在整個(gè)作用域內(nèi)都是可見的;

在編譯的時(shí)候,因?yàn)?nbsp;x = 2 這條語句,所以知道函數(shù)中存在一個(gè)局部變量 x,那么查找的時(shí)候就會(huì)在當(dāng)前局部作用域中查找。但還沒來得及賦值,就 print(x) 了,換句話說,在打印 x 的時(shí)候,它還沒有和某個(gè)具體的值進(jìn)行綁定,所以報(bào)錯(cuò):局部變量 x 在賦值之前就被使用了。

但如果沒有 x = 2 這條語句則不會(huì)報(bào)錯(cuò),因?yàn)橹谰植孔饔糜蛑胁淮嬖?x 這個(gè)變量,所以會(huì)找全局變量 x,從而打印 1。

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

import dis

x = 1

def foo():
    print(x)

dis.dis(foo)
"""
  5           0 RESUME                   0

  6           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_GLOBAL              2 (x)
             22 CALL                     1
             30 POP_TOP
             32 RETURN_CONST             0 (None)
"""

def bar():
    print(x)
    x = 2

dis.dis(bar)
"""
 10           0 RESUME                   0

 11           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_FAST_CHECK          0 (x)
             14 CALL                     1
             22 POP_TOP

 12          24 LOAD_CONST               1 (2)
             26 STORE_FAST               0 (x)
             28 RETURN_CONST             0 (None)
"""

第二列的序號(hào)代表字節(jié)碼指令的偏移量,我們看偏移量為 12 的指令,函數(shù) foo 對(duì)應(yīng)的指令是 LOAD_GLOBAL,意思是在 global 空間中查找 x。而函數(shù) bar 的指令是 LOAD_FAST_CHECK,表示在數(shù)組中靜態(tài)查找 x,但遺憾的是,此時(shí) x 還沒有和某個(gè)值進(jìn)行綁定。

因此結(jié)果說明 Python 采用了靜態(tài)作用域策略,在編譯的時(shí)候就已經(jīng)知道變量藏身于何處。而且這個(gè)例子也表明,一旦函數(shù)內(nèi)有了對(duì)某個(gè)變量的賦值操作,它會(huì)在整個(gè)作用域內(nèi)可見,因?yàn)榫幾g時(shí)就已經(jīng)確定。換句話說,會(huì)遮蔽外層作用域中相同的名字。

我們看一下函數(shù) foo 和函數(shù) bar 的符號(hào)表。

x = 1

def foo():
    print(x)


def bar():
    print(x)
    x = 2

print(foo.__code__.co_varnames)  # ()
print(bar.__code__.co_varnames)  # ('x',)

在編譯的時(shí)候,就知道函數(shù) bar 里面存在局部變量 x。

如果想修復(fù)這個(gè)錯(cuò)誤,可以用之前說的 global 關(guān)鍵字,將變量 x 聲明為全局的。

x = 1

def bar():
    global x  # 表示變量 x 是全局變量
    print(x)
    x = 2

bar()  # 1
print(x)  # 2

但這樣的話,會(huì)導(dǎo)致外部的全局變量被修改,如果不想出現(xiàn)這種情況,那么可以考慮直接獲取全局名字空間。

x = 1

def bar():
    print(globals()["x"])
    x = 2

bar()  # 1
print(x)  # 1

這樣結(jié)果就沒問題了,同樣的,類似的問題也會(huì)出現(xiàn)在嵌套函數(shù)中。

def foo():
    x = 1
    def bar():
        print(x)
        x = 2
    return bar

foo()()

執(zhí)行內(nèi)層函數(shù) bar 的時(shí)候,print(x) 也會(huì)出現(xiàn) UnboundLocalError,如果想讓它不報(bào)錯(cuò),而是打印外層函數(shù)中的 x,該怎么做呢?Python 同樣為我們準(zhǔn)備了一個(gè)關(guān)鍵字: nonlocal。

def foo():
    x = 1
    def bar():
        # 使用 nonlocal 的時(shí)候,必須是在內(nèi)層函數(shù)里面
        nonlocal x
        print(x)
        x = 2
    return bar

foo()()  # 1

如果 bar 里面是 global x,那么表示 x 是全局變量,當(dāng) foo()() 執(zhí)行完畢之后,會(huì)創(chuàng)建一個(gè)全局變量 x = 2。但這里不是 global,而是 nonlocal,表示 x 是外部作用域中的變量,因此會(huì)打印 foo 里面的變量 x。

當(dāng)然啦,既然聲明為 nonlocal,那么 foo 里面的 x 肯定會(huì)受到影響。

from types import FrameType
import inspect

frame: FrameType | None = None  

def foo():
    globals()["frame"] = inspect.currentframe()
    x = 1
    def bar():
        nonlocal x
        # print(x)
        x = 2
    return bar

bar = foo()
# 打印 foo 的局部變量,此時(shí)變量 x 的值為 1
print(frame.f_locals)
"""
{'bar': <function foo.<locals>.bar at 0x0000021EC32AB9A0>, 'x': 1}
"""
# 調(diào)用內(nèi)層函數(shù) bar
bar()
# 此時(shí) foo 的局部變量 x 的值變成了 2
print(frame.f_locals)
"""
{'bar': <function foo.<locals>.bar at 0x0000021EC32AB9A0>, 'x': 2}
"""

不過由于 foo 是一個(gè)函數(shù),調(diào)用內(nèi)層函數(shù) bar 的時(shí)候,外層函數(shù) foo 已經(jīng)結(jié)束了,所以不管怎么修改它里面的變量,都無所謂了。

另外上面的函數(shù)只嵌套了兩層,即使嵌套很多層也是可以的。

from types import FrameType
import inspect

frame: FrameType | None = None

def a():
    def b():
        globals()["frame"] = inspect.currentframe()
        x = 123
        def c():
            def d():
                def e():
                    def f():
                        nonlocal x
                        print(x)
                        x = 456
                    return f
                return e
            return d
        return c
    return b

b = a()
c = b()
d = c()
e = d()
f = e()
print(frame.f_locals)
"""
{'c': <function a.<locals>.b.<locals>.c at 0x00000255A0C10F70>, 'x': 123}
"""
# 調(diào)用函數(shù) f 的時(shí)候,打印的是函數(shù) b 里面的變量 x
# 當(dāng)然,最后也會(huì)修改它
f()
"""
123
"""
print(frame.f_locals)
"""
{'c': <function a.<locals>.b.<locals>.c at 0x00000255A0C10F70>, 'x': 456}
"""

不難發(fā)現(xiàn),在嵌套多層的情況下,會(huì)采用就近原則。如果函數(shù) d 里面也定義了變量 x,那么函數(shù) f 里面的 nonlocal x 表示的就是函數(shù) d 里面的局部變量 x。 

屬性查找

當(dāng)我們?cè)L問某個(gè)變量時(shí),會(huì)按照 LEGB 的規(guī)則進(jìn)行查找,而屬性查找也是類似的,本質(zhì)上都是到名字空間中查找一個(gè)名字所引用的對(duì)象。但由于屬性查找限定了范圍,所以要更簡(jiǎn)單,比如 a.xxx,就是到 a 里面去找屬性 xxx,這個(gè)規(guī)則是不受 LEGB 作用域限制的,就是到 a 里面查找,有就是有,沒有就是沒有。

import numpy as np

# 在 np 指向的對(duì)象(模塊)中查找 array 屬性
print(np.array([1, 2, 3]))
"""
[1 2 3]
"""
# 本質(zhì)上就是去 np 的屬性字典中查找 key = "array"
print(np.__dict__["array"]([11, 22, 33]))
"""
[11 22 33]
"""


class Girl:

    name = "古明地覺"
    age = 16

print(Girl.name, Girl.age)
"""
古明地覺 16
"""
print(Girl.__dict__["name"], Girl.__dict__["age"])
"""
古明地覺 16
"""

需要補(bǔ)充一點(diǎn),我們說屬性查找會(huì)按照 LEGB 規(guī)則,但這必須限制在自身所在的模塊內(nèi),如果是多個(gè)模塊就不行了。舉個(gè)例子,假設(shè)有兩個(gè) py 文件,內(nèi)容如下:

# girl.py
print(name)

# main.py
name = "古明地覺"
from girl import name

關(guān)于模塊的導(dǎo)入我們后續(xù)會(huì)詳細(xì)說,總之執(zhí)行 main.py 的時(shí)候報(bào)錯(cuò)了,提示變量 name 沒有被定義,但問題是 main.py 里面定義了變量 name,為啥報(bào)錯(cuò)呢?

很明顯,因?yàn)?girl.py 里面沒有定義變量 name,所以導(dǎo)入 girl 的時(shí)候報(bào)錯(cuò)了。因此結(jié)論很清晰了,變量查找雖然是 LEGB 規(guī)則,但不會(huì)越過自身所在的模塊。print(name) 在 girl.py 里面,而變量 name 定義在 main.py 里面,在導(dǎo)入時(shí)不可能跨過 girl.py 的作用域去訪問 main.py 里的 name,因此在執(zhí)行 from girl import name 的時(shí)候會(huì)拋出  NameError。

雖然每個(gè)模塊內(nèi)部的作用域規(guī)則有點(diǎn)復(fù)雜,因?yàn)橐裱?LEGB;但模塊與模塊的作用域之間則劃分得很清晰,就是相互獨(dú)立。

關(guān)于模塊,我們后續(xù)會(huì)詳細(xì)說。總之通過屬性操作符 . 的方式,本質(zhì)上都是去指定的名字空間中查找對(duì)應(yīng)的屬性。

屬性空間

我們知道,自定義的類里面如果沒有 __slots__,那么這個(gè)類的實(shí)例對(duì)象會(huì)有一個(gè)屬性字典,和名字空間的概念是等價(jià)的。

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

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

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

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

當(dāng)然模塊也有屬性字典,本質(zhì)上和類的實(shí)例對(duì)象是一致的,因?yàn)槟K本身就是一個(gè)實(shí)例對(duì)象。

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

另外這個(gè) __builtins__ 位于 global 名字空間里面,然后獲取 global 名字空間的 globals 又是一個(gè)內(nèi)置函數(shù),于是一個(gè)神奇的事情就出現(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é)

整個(gè)內(nèi)容很好理解,關(guān)鍵的地方就在于局部變量,它是靜態(tài)存儲(chǔ)的,編譯期間就已經(jīng)確定。而在訪問局部變量時(shí),也是基于數(shù)組實(shí)現(xiàn)的靜態(tài)查找,而不是使用字典。

關(guān)于 local 空間,以及如何使用數(shù)組靜態(tài)查找,我們后面還會(huì)詳細(xì)說。

責(zé)任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2024-05-21 12:51:06

Python對(duì)象PyObject

2024-05-22 13:04:46

Python對(duì)象關(guān)系

2020-09-06 22:59:35

Linux文件命令

2021-03-10 07:52:58

虛擬機(jī)程序VMware

2021-07-31 12:58:53

PodmanLinux虛擬機(jī)

2022-01-26 16:30:47

代碼虛擬機(jī)Linux

2018-06-22 10:30:56

C語言虛擬機(jī)編譯器

2015-04-28 08:11:27

2013-12-09 15:35:44

Docker虛擬機(jī)

2012-07-03 13:15:00

vSphere虛擬機(jī)存儲(chǔ)

2023-10-30 08:45:55

Spring容器攔截

2013-07-09 10:44:05

PowerShell

2015-07-29 15:05:01

2014-08-18 14:58:25

微軟IE

2013-07-23 14:48:19

PowerShell

2018-04-02 09:49:51

數(shù)據(jù)備份

2020-12-17 12:27:52

Git文件名React

2016-12-07 17:45:44

Linux文件

2013-03-08 02:52:03

個(gè)人開發(fā)項(xiàng)目糾錯(cuò)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)