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

名字空間:Python 變量的容身之所

開發(fā) 前端
名字空間是 Python 的靈魂,它規(guī)定了一個(gè)變量應(yīng)該如何查找,關(guān)于變量查找,下一篇文章來(lái)詳細(xì)介紹,到時(shí)你會(huì)對(duì)名字空間有更加透徹的理解。

楔子

在介紹棧楨的時(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)置名字空間了。

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

2009-12-23 14:11:05

WPF名字空間

2009-10-13 14:29:49

VB.NET名字空間

2024-11-14 08:10:00

變量命名開發(fā)

2024-10-28 12:06:09

2019-08-26 19:24:55

Podman容器Linux

2010-02-05 10:08:55

C++名字空間

2009-11-04 13:50:55

VB.NET名字空間

2014-12-09 11:20:48

Docker網(wǎng)絡(luò)名字空間

2024-05-16 08:23:26

大語(yǔ)言模型知識(shí)圖譜人工智能

2011-03-03 10:45:34

PureftpdMYSQL

2021-03-25 12:00:18

Python變量常量

2011-11-22 13:28:24

華為

2020-10-21 07:07:34

Java內(nèi)存空間

2018-02-01 16:26:44

面試題static變量

2010-03-23 11:32:56

python環(huán)境運(yùn)行

2011-07-12 17:06:43

PHP

2020-02-21 14:55:02

Python代碼字符串

2011-04-28 11:21:58

音響家庭影院

2013-09-05 10:07:34

javaScript變量

2013-07-25 15:15:26

iOS開發(fā)學(xué)習(xí)iOS全局變量
點(diǎn)贊
收藏

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