成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

當查找一個 Python 變量時,虛擬機會進行哪些動作?

開發 前端
整個內容很好理解,關鍵的地方就在于局部變量,它是靜態存儲的,編譯期間就已經確定。而在訪問局部變量時,也是基于數組實現的靜態查找,而不是使用字典。

楔子

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

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

f1()  # name 'name' is not defined

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

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

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

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

LEGB 規則

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

x = 1

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

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

調用了內層函數 bar,如果按照 LGB 的規則來查找的話,由于函數 bar 的作用域沒有 a,那么應該到全局里面找,打印的結果是 1 才對。

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

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

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

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

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

global 表達式

在初學 Python 時,估計很多人都會對下面的問題感到困惑。

x = 1

def foo():
    print(x)

foo()
"""
1
"""

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

x = 1

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

foo()

這段代碼在執行 print(x) 的時候是會報錯的,會拋出一個 UnboundLocalError。

圖片圖片

意思就是說,無法訪問局部變量 x,因為它還沒有和某個值(對象)進行綁定。當然,如果是以前的 Python 版本,比如 3.8,同樣會拋出這個錯誤,只是信息不同。

圖片圖片

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

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

  • 函數中的變量是靜態存儲、靜態訪問的, 內部有哪些變量在編譯的時候就已經確定;
  • 局部變量在整個作用域內都是可見的;

在編譯的時候,因為 x = 2 這條語句,所以知道函數中存在一個局部變量 x,那么查找的時候就會在當前局部作用域中查找。但還沒來得及賦值,就 print(x) 了,換句話說,在打印 x 的時候,它還沒有和某個具體的值進行綁定,所以報錯:局部變量 x 在賦值之前就被使用了。

但如果沒有 x = 2 這條語句則不會報錯,因為知道局部作用域中不存在 x 這個變量,所以會找全局變量 x,從而打印 1。

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

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

第二列的序號代表字節碼指令的偏移量,我們看偏移量為 12 的指令,函數 foo 對應的指令是 LOAD_GLOBAL,意思是在 global 空間中查找 x。而函數 bar 的指令是 LOAD_FAST_CHECK,表示在數組中靜態查找 x,但遺憾的是,此時 x 還沒有和某個值進行綁定。

因此結果說明 Python 采用了靜態作用域策略,在編譯的時候就已經知道變量藏身于何處。而且這個例子也表明,一旦函數內有了對某個變量的賦值操作,它會在整個作用域內可見,因為編譯時就已經確定。換句話說,會遮蔽外層作用域中相同的名字。

我們看一下函數 foo 和函數 bar 的符號表。

x = 1

def foo():
    print(x)


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

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

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

如果想修復這個錯誤,可以用之前說的 global 關鍵字,將變量 x 聲明為全局的。

x = 1

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

bar()  # 1
print(x)  # 2

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

x = 1

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

bar()  # 1
print(x)  # 1

這樣結果就沒問題了,同樣的,類似的問題也會出現在嵌套函數中。

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

foo()()

執行內層函數 bar 的時候,print(x) 也會出現 UnboundLocalError,如果想讓它不報錯,而是打印外層函數中的 x,該怎么做呢?Python 同樣為我們準備了一個關鍵字: nonlocal。

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

foo()()  # 1

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

當然啦,既然聲明為 nonlocal,那么 foo 里面的 x 肯定會受到影響。

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 的局部變量,此時變量 x 的值為 1
print(frame.f_locals)
"""
{'bar': <function foo.<locals>.bar at 0x0000021EC32AB9A0>, 'x': 1}
"""
# 調用內層函數 bar
bar()
# 此時 foo 的局部變量 x 的值變成了 2
print(frame.f_locals)
"""
{'bar': <function foo.<locals>.bar at 0x0000021EC32AB9A0>, 'x': 2}
"""

不過由于 foo 是一個函數,調用內層函數 bar 的時候,外層函數 foo 已經結束了,所以不管怎么修改它里面的變量,都無所謂了。

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

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}
"""
# 調用函數 f 的時候,打印的是函數 b 里面的變量 x
# 當然,最后也會修改它
f()
"""
123
"""
print(frame.f_locals)
"""
{'c': <function a.<locals>.b.<locals>.c at 0x00000255A0C10F70>, 'x': 456}
"""

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

屬性查找

當我們訪問某個變量時,會按照 LEGB 的規則進行查找,而屬性查找也是類似的,本質上都是到名字空間中查找一個名字所引用的對象。但由于屬性查找限定了范圍,所以要更簡單,比如 a.xxx,就是到 a 里面去找屬性 xxx,這個規則是不受 LEGB 作用域限制的,就是到 a 里面查找,有就是有,沒有就是沒有。

import numpy as np

# 在 np 指向的對象(模塊)中查找 array 屬性
print(np.array([1, 2, 3]))
"""
[1 2 3]
"""
# 本質上就是去 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
"""

需要補充一點,我們說屬性查找會按照 LEGB 規則,但這必須限制在自身所在的模塊內,如果是多個模塊就不行了。舉個例子,假設有兩個 py 文件,內容如下:

# girl.py
print(name)

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

關于模塊的導入我們后續會詳細說,總之執行 main.py 的時候報錯了,提示變量 name 沒有被定義,但問題是 main.py 里面定義了變量 name,為啥報錯呢?

很明顯,因為 girl.py 里面沒有定義變量 name,所以導入 girl 的時候報錯了。因此結論很清晰了,變量查找雖然是 LEGB 規則,但不會越過自身所在的模塊。print(name) 在 girl.py 里面,而變量 name 定義在 main.py 里面,在導入時不可能跨過 girl.py 的作用域去訪問 main.py 里的 name,因此在執行 from girl import name 的時候會拋出  NameError。

雖然每個模塊內部的作用域規則有點復雜,因為要遵循 LEGB;但模塊與模塊的作用域之間則劃分得很清晰,就是相互獨立。

關于模塊,我們后續會詳細說。總之通過屬性操作符 . 的方式,本質上都是去指定的名字空間中查找對應的屬性。

屬性空間

我們知道,自定義的類里面如果沒有 __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

當然模塊也有屬性字典,本質上和類的實例對象是一致的,因為模塊本身就是一個實例對象。

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

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

小結

整個內容很好理解,關鍵的地方就在于局部變量,它是靜態存儲的,編譯期間就已經確定。而在訪問局部變量時,也是基于數組實現的靜態查找,而不是使用字典。

關于 local 空間,以及如何使用數組靜態查找,我們后面還會詳細說。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-05-22 13:04:46

Python對象關系

2024-05-21 12:51:06

Python對象PyObject

2020-09-06 22:59:35

Linux文件命令

2022-01-26 16:30:47

代碼虛擬機Linux

2021-03-10 07:52:58

虛擬機程序VMware

2021-07-31 12:58:53

PodmanLinux虛擬機

2015-04-28 08:11:27

2018-06-22 10:30:56

C語言虛擬機編譯器

2023-10-30 08:45:55

Spring容器攔截

2012-07-03 13:15:00

vSphere虛擬機存儲

2013-12-09 15:35:44

Docker虛擬機

2013-07-09 10:44:05

PowerShell

2014-08-18 14:58:25

微軟IE

2015-07-29 15:05:01

2013-07-23 14:48:19

PowerShell

2018-04-02 09:49:51

數據備份

2014-11-19 13:06:59

2025-04-27 03:33:00

2016-12-07 17:45:44

Linux文件

2010-02-05 13:44:36

Dalvik虛擬機
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品久久久久久久 | www日韩高清| 日韩视频在线免费观看 | 成人在线视频免费播放 | 91网站在线看 | 久久久久国产精品www | 欧美日韩高清在线一区 | 四虎影院在线观看av | 99热激情| 精品久久久久久亚洲精品 | 国产免费一区二区 | 久久99国产精品 | 成人国产精品色哟哟 | 亚洲一区视频在线 | 一区二区在线免费播放 | 亚洲电影专区 | 中文字幕乱码视频32 | 亚洲男人网 | 日韩精品一区二区三区视频播放 | 水蜜桃久久夜色精品一区 | 97久久国产| 午夜精品久久久久久久久久久久 | 99久久精品国产一区二区三区 | 久久久久久久av | 一级aaaaaa毛片免费同男同女 | 一a级片| 有码在线 | 久久国产精品久久久久久久久久 | 国产精品国产精品 | 毛片a级毛片免费播放100 | 欧美在线成人影院 | 日日干综合 | 国产欧美日韩视频 | 中文字幕 欧美 日韩 | 亚洲精品久久久 | 成人精品国产一区二区4080 | 色综合久 | 久久国产精品视频 | 日韩看片 | 国产成人免费一区二区60岁 | 天天操网 |