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

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

開發 前端
名字空間是 Python 的靈魂,它規定了一個變量應該如何查找,關于變量查找,下一篇文章來詳細介紹,到時你會對名字空間有更加透徹的理解。

楔子

在介紹棧楨的時候,我們看到了 3 個獨立的名字空間:f_locals、f_globals、f_builtins。名字空間對 Python 來說是一個非常重要的概念,虛擬機的運行機制和名字空間有著非常緊密的聯系。并且在 Python 中,與名字空間這個概念緊密聯系在一起的還有名字、作用域這些概念,下面我們就來剖析這些概念是如何體現的。

變量只是一個名字

在這個系列的最開始我們就說過,從解釋器的角度來看,變量只是一個泛型指針 PyObject *,而從 Python 的角度來看,變量只是一個名字、或者說符號,用于和對象進行綁定的。

name = "古明地覺"

上面這個賦值語句其實就是將 name 和 "古明地覺" 綁定起來,讓我們可以通過 name 這個符號找到對應的 PyUnicodeObject。因此定義一個變量,本質上就是建立名字和對象之間的映射關系。

另外我們說 Python 雖然一切皆對象,但拿到的都是指向對象的指針,因此創建函數和類,以及模塊導入,同樣是在完成名字和對象的綁定。

def foo(): pass

class A(): pass

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

import os

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

當我們導入一個模塊的時候,解釋器是這么做的。

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

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

總結:無論是普通的賦值語句,還是定義函數和類,亦或是模塊導入,它們本質上都是在完成變量和對象的綁定。

name = "古明地覺"

def foo(): pass

class A(): pass

import os
import numpy as np

里面的 name、foo、A、os、np,都只是一個變量,或者說名字、符號,然后通過名字可以獲取與之綁定的對象。

作用域和名字空間

正如上面所說,賦值語句、函數定義、類定義、模塊導入,本質上只是完成了變量和對象之間的綁定,或者說我們創建了變量到對象的映射,通過變量可以獲取對應的對象,而它們的容身之所就是名字空間。

所以名字空間是通過 PyDictObject 對象實現的,這對于映射來說簡直再適合不過了。而前面介紹字典的時候,我們說字典是被高度優化的,原因就是虛擬機本身也重度依賴字典,從這里的名字空間即可得到體現。

當然,在一個模塊內部,變量還存在可見性的問題,比如:

x = 1

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

foo()
print(x)  # 1

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

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

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

關于作用域這個概念,我們要記住:它僅僅是由源代碼的文本所決定。在 Python 中,一個變量在某個位置是否起作用,是由它的文本位置決定的。

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

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

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

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

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

class A:
    x = 1
    
class B:
    y = 2
    print(A.x)  # 1
    print(y)  # 2

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

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

x = 1

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

foo()
print(x)  # 1

還是上面的代碼,如果我們把函數里面的 a = 2 給刪掉,意味著函數的作用域里面已經沒有 a 這個變量了,那么再執行程序會有什么后果呢?從 Python 層面來看,顯然是會尋找外部的 a。因此我們可以得到如下結論:

  • 作用域是層層嵌套的;
  • 內層作用域可以訪問外層作用域;
  • 外層作用域無法訪問內層作用域,如果是把外層的 a = 1 給去掉,那么最后面的 print(a) 鐵定報錯;
  • 查找元素會依次從當前作用域向外查找,也就是查找元素時,對應的作用域是按照從小往大、從里往外的方向前進的;

global 名字空間

不光函數、類有自己的作用域,模塊對應的源文件本身也有相應的作用域。比如:

name = "古明地覺"
age = 16

def foo():
    return 123

class A:
    pass

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

而 Python 也提供了 globals 函數,用于獲取 global 名字空間。

name = "古明地覺"

def foo():
    pass

print(globals())
"""
{..., 'name': '古明地覺', 'foo': <function foo at 0x0000015255143E20>}
"""

里面的 ... 表示省略了一部分輸出,我們看到創建的全局變量就在里面。而且 foo 也是一個變量,它指向一個函數對象。

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

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

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

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

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

def foo1():
    def foo2():
        def foo3():
            globals()["age"] = 16
        return foo3
    return foo2

foo1()()()
print(age)  # 16

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

所以即使是在函數中給 global 名字空間添加一個鍵值對,也等價于定義一個全局變量。

圖片圖片

問題來了,如果在函數里面,我們不獲取 global 名字空間,怎么創建全局變量呢?

name = "古明地覺"

def foo():
    global name
    name = "古明地戀"

print(name)  # 古明地覺
foo()
print(name)  # 古明地戀

很簡單,Python 為我們準備了 global 關鍵字,表示聲明的變量是全局的。

local 名字空間

像函數和類擁有的作用域,我們稱之為 local 作用域,在運行時會對應 local 名字空間,即局部名字空間。由于不同的函數具有不同的作用域,所以局部名字空間可以有很多個,但全局名字空間只有一個。

對于 local 名字空間來說,它也對應一個字典,顯然這個字典就不是全局唯一的了。而如果想獲取局部名字空間,Python 也提供了 locals 函數。

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}

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

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

注:關于 local 名字空間,還有一個重要的細節,全局變量會存儲在 global 名字空間中,但局部變量卻并不存儲在 local 名字空間中。函數有哪些局部變量在編譯的時候就已經確定了,會被靜態存儲在數組中,關于這一點,后續會單獨詳細說明。

builtin 名字空間

Python 有一個所謂的 LGB 規則,指的是在查找一個變量時,會按照自身的 local 空間、外層的 global 空間、內置的 builtin 空間的順序進行查找。

builtin 名字空間也是一個字典,當 local 名字空間、global 名字空間都查找不到指定變量的時候,會去 builtin 空間查找。而關于 builtin 空間的獲取,Python 提供了一個模塊。

# 等價于 __builtins__
import builtins
print(builtins is __builtins__)  # True
print(builtins)  # <module 'builtins' (built-in)>

builtins 是一個模塊,那么 builtins.__dict__ 便是 builtin 名字空間,也叫內置名字空間。

import builtins

# builtins.list 表示從 builtin 名字空間中查找 list
# 它等價于 builtins.__dict__["list"]
# 而如果只寫 list,那么由于 local 空間、global 空間都沒有
# 因此最終還是會從 builtin 空間中查找
# 但如果是 builtins.list,那么就不兜圈子了
# 表示:"builtin 空間,就從你這里獲取了"
print(builtins.list is list)  # True


# 將 builtin 空間的 dict 改成 123
builtins.dict = 123
# 那么此時獲取的 dict 就是 123
print(dict + 456)  # 579


# 如果是 str = 123,等價于創建全局變量 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 要快,為什么?

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

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

exec 和 eval

記得之前介紹 exec 和 eval 的時候,我們說這兩個函數里面還可以接收第二個參數和第三個參數,它們分別表示 global 名字空間、local 名字空間。

# 如果不指定,默認是當前所在的名字空間
# 顯然此時是全局名字空間
exec("name = '古明地覺'")
print(name)  # 古明地覺

# 但我們也可以指定某個名字空間
namespace = {}
# 比如將 namespace 作為全局名字空間
# 這里我們沒有指定第三個參數,也就是局部名字空間
# 如果指定了第二個參數,但沒有指定第三個參數
# 那么第三個參數默認和第二個參數保持一致
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

所以名字空間本質上就是一個字典,所謂的變量不過是字典里面的一個 key。為了進一步加深印象,再舉個模塊的例子:

# 我們自定義一個模塊吧
# 首先模塊也是一個對象,類型為 <class 'module'>
# 但底層沒有將這個類暴露給我們,所以需要換一種方式獲取
import sys
ModuleType = type(sys)

# 以上就拿到了模塊的類型對象,調用即可得到模塊對象
# 這里我們自定義一個類,繼承 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 '虛無之境'>"

my_module = MyModule("自定義模塊")
print(my_module)
"""
<module '自定義模塊' from '虛無之境'>
"""

# 此時的 my_module 啥也沒有,我們為其添磚加瓦
my_module.__dict__["name"] = "古明地覺"
print(my_module.name)  # 古明地覺

# 給模塊設置屬性,本質上也是操作模塊的屬性字典,當然獲取屬性也是如此
# 如果再和 exec 結合的話
code_string = """
age = 16
def foo():
    return "我是函數 foo"
    
from functools import reduce     
"""
# 此時屬性就設置在了模塊的屬性字典里面
exec(code_string, my_module.__dict__)
# 然后我們獲取它
print(my_module.age)  # 16
print(my_module.foo())  # 我是函數 foo
print(my_module.reduce(int.__add__, range(101)))  # 5050

# 是不是很神奇呢?由于 my_module 是一個模塊對象
# 我們還可以將它注入到 sys.modules 中,然后通過 import 獲取
sys.modules["俺滴模塊"] = my_module
from 俺滴模塊 import name, age, foo
print(name)  # 古明地覺
print(age)  # 16
print(foo())  # 我是函數 foo

怎么樣,是不是很有意思呢?相信你對名字空間已經有了足夠清晰的認識,它是變量和與之綁定的對象的容身之所。

小結

名字空間是 Python 的靈魂,它規定了一個變量應該如何查找,關于變量查找,到時你會對名字空間有更加透徹的理解。

然后是作用域,所謂名字空間其實就是作用域的動態體現。整個 py 文件是一個作用域,也是全局作用域;定義函數、定義類、定義方法,又會創建新的作用域,這些作用域層層嵌套。

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

然后全局名字空間是一個字典,它是唯一的,操作里面的鍵值對等價于操作全局變量;至于局部名字空間則不唯一,每一個函數都有自己的局部名字空間,但我們要知道函數內部在訪問局部變量的時候是靜態訪問的(相關細節后續聊)。

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

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

2009-12-23 14:11:05

WPF名字空間

2009-10-13 14:29:49

VB.NET名字空間

2024-11-14 08:10:00

變量命名開發

2024-10-28 12:06:09

2010-02-05 10:08:55

C++名字空間

2009-11-04 13:50:55

VB.NET名字空間

2019-08-26 19:24:55

Podman容器Linux

2014-12-09 11:20:48

Docker網絡名字空間

2024-05-16 08:23:26

大語言模型知識圖譜人工智能

2011-03-03 10:45:34

PureftpdMYSQL

2011-11-22 13:28:24

華為

2020-10-21 07:07:34

Java內存空間

2021-03-25 12:00:18

Python變量常量

2010-03-23 11:32:56

python環境運行

2020-02-21 14:55:02

Python代碼字符串

2018-02-01 16:26:44

面試題static變量

2011-07-12 17:06:43

PHP

2011-10-17 08:29:33

Ubuntu 11.1思考

2011-04-08 16:38:34

開源

2011-12-06 15:12:01

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 一级黄色毛片 | 黄一级| 特黄级国产片 | 久久青视频 | 亚洲高清视频在线 | 国产在线中文字幕 | 9久久婷婷国产综合精品性色 | 国产精品成人国产乱一区 | 午夜欧美 | 国产精品揄拍一区二区久久国内亚洲精 | 亚洲一区二区视频 | 在线成人免费视频 | 亚洲激精日韩激精欧美精品 | 国产成人在线观看免费 | 国产精品视频不卡 | 日韩三区在线观看 | 亚洲欧美日韩精品久久亚洲区 | 一级黄色片美国 | 日韩一二三| 操操日| 午夜欧美一区二区三区在线播放 | 午夜精品久久久久久 | 国产精品国产成人国产三级 | 大吊一区二区 | 在线精品亚洲欧美日韩国产 | 成人福利在线观看 | 欧美日韩亚洲视频 | 国产一区二区a | 欧美精品一区二区在线观看 | 91在线观看| 久久精品国产99国产精品 | 超碰成人免费 | 欧美视频免费在线 | 免费在线一区二区三区 | 国产午夜亚洲精品不卡 | 91精品在线观看入口 | 九九热国产视频 | 一区二区三区福利视频 | 日韩中文字幕在线观看 | 成人国产在线视频 | 亚洲巨乳自拍在线视频 |