Python學(xué)習(xí)之名字,作用域,名字空間
前言:
我們?cè)赑yFrameObject里面看到了3個(gè)獨(dú)立的名字空間:f_locals、f_globals、f_builtins。名字空間對(duì)于Python來(lái)說(shuō)是一個(gè)非常重要的概念,Python虛擬機(jī)的運(yùn)行機(jī)制和名字空間有著非常緊密的聯(lián)系。并且在Python中,與名字空間這個(gè)概念緊密聯(lián)系在一起的還有名字、作用域這些概念,下面就來(lái)剖析這些概念是如何體現(xiàn)的。
變量只是一個(gè)名字
很早的時(shí)候我們就說(shuō)過(guò),從解釋器的角度來(lái)看,變量只是一個(gè)泛型指針PyObject *,而從Python的角度來(lái)看,變量只是一個(gè)名字、或者說(shuō)符號(hào),用于和對(duì)象進(jìn)行綁定的。
變量的定義本質(zhì)上就是建立名字和對(duì)象之間的約束關(guān)系,所以a = 1這個(gè)賦值語(yǔ)句本質(zhì)上就是將a和1綁定起來(lái),讓我們通過(guò)a這個(gè)符號(hào)可以找到對(duì)應(yīng)的PyLongObject。
除了變量賦值,創(chuàng)建函數(shù)、類也相當(dāng)于定義變量,或者說(shuō)完成名字和對(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ì)象。
import numpy as np當(dāng)中的as語(yǔ)句同樣是在定義變量,將名字np和對(duì)應(yīng)的模塊對(duì)象綁定起來(lái),以后就可以通過(guò)np這個(gè)名字去獲取指定的模塊了。
另外,當(dāng)我們導(dǎo)入一個(gè)模塊的時(shí)候,解釋器是這么做的。比如:import os等價(jià)于os=__import__("os"),可以看到本質(zhì)上還是一個(gè)賦值語(yǔ)句。
作用域和名字空間
我們說(shuō)賦值語(yǔ)句、函數(shù)定義、類定義、模塊導(dǎo)入,本質(zhì)上只是完成了名字和對(duì)象之間的綁定。而從概念上講,我們實(shí)際上得到了一個(gè)name和obj之間的映射關(guān)系,通過(guò)name可以獲取對(duì)應(yīng)的obj,而它們的容身之所就是名字空間。
所以名字空間是通過(guò)PyDictObject對(duì)象實(shí)現(xiàn)的,這對(duì)于映射來(lái)說(shuō)簡(jiǎn)直再適合不過(guò)了。而在前面介紹字典的時(shí)候,我們說(shuō)過(guò)字典是被高度優(yōu)化的,原因就是虛擬機(jī)本身也重度依賴字典,從這里的名字空間即可得到體現(xiàn)。
但是一個(gè)模塊內(nèi)部,名字還存在可見(jiàn)性的問(wèn)題,比如:
a = 1 def foo(): a = 2 print(a) # 2 foo() print(a) # 1
我們看到同一個(gè)變量名,打印的確是不同的值,說(shuō)明指向了不同的對(duì)象,換句話說(shuō)這兩個(gè)變量是在不同的名字空間中被創(chuàng)建的。
然后我們知道名字空間本質(zhì)上是一個(gè)字典,如果兩者是在同一個(gè)名字空間,那么由于字典的key的不重復(fù)性,當(dāng)執(zhí)行a=2的時(shí)候,會(huì)把字典里面key為a的value給更新成2。但是在外面還是打印1,這說(shuō)明兩者所在的不是同一個(gè)名字空間,打印的也就自然不是同一個(gè)a。
因此對(duì)于一個(gè)模塊而言,內(nèi)部是可能存在多個(gè)名字空間的,每一個(gè)名字空間都與一個(gè)作用域相對(duì)應(yīng)。作用域就可以理解為一段程序的正文區(qū)域,在這個(gè)區(qū)域里面定義的變量是有意義的,然而一旦出了這個(gè)區(qū)域,就無(wú)效了。
對(duì)于作用域這個(gè)概念,至關(guān)重要的是要記?。核鼉H僅是由源程序的文本所決定的。在Python中,一個(gè)變量在某個(gè)位置是否起作用,是由它的文本位置決定的。
因此Python具有靜態(tài)作用域(詞法作用域),而名字空間則是作用域的動(dòng)態(tài)體現(xiàn),一個(gè)由程序文本定義的作用域在Python運(yùn)行時(shí)會(huì)轉(zhuǎn)化為一個(gè)名字空間、即一個(gè)PyDictObject對(duì)象。而進(jìn)入一個(gè)函數(shù),顯然進(jìn)入了一個(gè)新的作用域,因此函數(shù)在執(zhí)行時(shí),會(huì)創(chuàng)建一個(gè)名字空間。
我們之前說(shuō),在對(duì)Python源代碼進(jìn)行編譯的時(shí)候,對(duì)于代碼中的每一個(gè)block,都會(huì)創(chuàng)建一個(gè)PyCodeObject與之對(duì)應(yīng)。而當(dāng)進(jìn)入一個(gè)新的名字空間、或者說(shuō)作用域時(shí),我們就算是進(jìn)入一個(gè)新的block了。
而根據(jù)我們使用Python的經(jīng)驗(yàn),顯然函數(shù)、類都是一個(gè)新的block,當(dāng)Python運(yùn)行的時(shí)候會(huì)為它們創(chuàng)建各自的名字空間。
所以名字空間是名字、或者變量的上下文環(huán)境,名字的含義取決于名字空間。更具體的說(shuō),一個(gè)變量綁定的對(duì)象是不確定的,需要由名字空間來(lái)決定。
位于同一個(gè)作用域的代碼可以直接訪問(wèn)作用域中出現(xiàn)的名字,即所謂的直接訪問(wèn);但不同作用域,則需要通過(guò)訪問(wèn)修飾符 . 進(jìn)行屬性訪問(wèn)。
class A: a = 1 class B: b = 2 print(A.a) # 1 print(b) # 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程序的行為。
a = 1 def foo(): a = 2 print(a) # 2 foo() print(a) # 1
還是上面的代碼,如果我們把函數(shù)里面的a=2給刪掉,意味著函數(shù)的作用域里面已經(jīng)沒(méi)有a這個(gè)變量了,那么再執(zhí)行程序會(huì)有什么后果呢?從Python層面來(lái)看,顯然是會(huì)尋找外部的a。
因此我們可以得到如下結(jié)論:
- 作用域是層層嵌套的;
- 內(nèi)層作用域可以訪問(wèn)外層作用域;
- 外層作用域無(wú)法訪問(wèn)內(nèi)層作用域,盡管我們沒(méi)有試,但是想都不用想。如果是把外層的a=1給去掉,那么最后面的print(a)鐵定報(bào)錯(cuò);
- 查找元素會(huì)依次從當(dāng)前作用域向外查找,也就是查找元素時(shí),對(duì)應(yīng)的作用域是按照從小往大、從里往外的方向前進(jìn)的;
LGB規(guī)則
我們說(shuō)函數(shù)、類有自己的作用域,但是模塊對(duì)應(yīng)的源文件本身也有相應(yīng)的作用域。比如:
name = "編程學(xué)習(xí)網(wǎng)" 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)。
而函數(shù)也有作用域,這個(gè)作用域稱為local作用域,對(duì)應(yīng)local名字空間;同時(shí)Python自身還定義了一個(gè)最頂層的作用域,也就是builtin作用域,像內(nèi)置函數(shù)、內(nèi)建對(duì)象都在builtin里面。
這三個(gè)作用域在Python2.2之前就存在了,所以那時(shí)候Python的作用域規(guī)則被稱之為L(zhǎng)GB規(guī)則:名字引用動(dòng)作沿著local作用域(local名字空間)、global作用域(global名字空間)、builtin作用域(builtin名字空間)來(lái)查找對(duì)應(yīng)的變量。
而獲取名字空間,Python也提供了相應(yīng)的內(nèi)置函數(shù):
- locals函數(shù):獲取當(dāng)前作用域的local名字空間,local名字空間也稱為局部名字空間;
- globals函數(shù):獲取當(dāng)前作用域的global名字空間,global名字空間也稱為全局名字空間;
- __builtins__函數(shù):或者import builtins,獲取當(dāng)前作用域的builtin名字空間,builtint名字空間也稱為內(nèi)置名字空間;
每個(gè)函數(shù)都有自己local名字空間,因?yàn)椴煌暮瘮?shù)對(duì)應(yīng)不同的作用域,但是global名字空間則是全局唯一。
name = "編程學(xué)習(xí)網(wǎng)" def foo(): pass print(globals()) # {..., 'name': '編程學(xué)習(xí)網(wǎng)', 'foo': <function foo at 0x000002977EDF61F0>}
里面的...表示省略了一部分輸出,我們看到創(chuàng)建的全局變量就在里面。而且foo也是一個(gè)變量,它指向一個(gè)函數(shù)對(duì)象。
但是注意,我們說(shuō)foo也是一個(gè)獨(dú)立的block,因此它會(huì)對(duì)應(yīng)一個(gè)PyCodeObject。但是在解釋到def foo的時(shí)候,會(huì)根據(jù)這個(gè)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í)行,這些留在介紹函數(shù)的時(shí)候再細(xì)說(shuō)。總之,我們看到foo也是一個(gè)全局變量,全局變量都在global名字空間中。
總之,global名字空間全局唯一,它是程序運(yùn)行時(shí)的全局變量和與之綁定的對(duì)象的容身之所,你在任何一個(gè)地方都可以訪問(wèn)到global名字空間。正如,你在任何一個(gè)地方都可以訪問(wèn)相應(yīng)的全局變量一樣。
此外,我們說(shuō)名字空間是一個(gè)字典,變量和對(duì)象會(huì)以鍵值對(duì)的形式存在里面。那么換句話說(shuō),如果我手動(dòng)地往這個(gè)global名字空間里面添加一個(gè)鍵值對(duì),是不是也等價(jià)于定義一個(gè)全局變量呢?
globals()["name"] = "編程學(xué)習(xí)網(wǎng)" print(name) # 編程學(xué)習(xí)網(wǎng) def f1(): def f2(): def f3(): globals()["age"] = 16 return f3 return f2 f1()()() print(age) # 16
我們看到確實(shí)如此,通過(guò)往global名字空間里面插入一個(gè)鍵值對(duì)完全等價(jià)于定義一個(gè)全局變量。并且global名字空間是唯一的,你在任何地方調(diào)用globals()得到的都是global名字空間,正如你在任何地方都可以訪問(wèn)到全局變量一樣。
所以即使是在函數(shù)中向global名字空間中插入一個(gè)鍵值對(duì),也等價(jià)于定義一個(gè)全局變量、并和對(duì)象綁定起來(lái)。
- name="xxx" 等價(jià)于 globals["name"]="xxx";
- print(name) 等價(jià)于 print(globals["name"]);
對(duì)于local名字空間來(lái)說(shuō),它也對(duì)應(yīng)一個(gè)字典,顯然這個(gè)字典就不是全局唯一的了,每一個(gè)局部作用域都會(huì)對(duì)應(yīng)自身的local名字空間。
def f(): name = "夏色祭" age = 16 return locals() def g(): name = "神樂(lè)mea" age = 38 return locals() print(locals() == globals()) # True print(f()) # {'name': '夏色祭', 'age': 16} print(g()) # {'name': '神樂(lè)mea', 'age': 38}
顯然對(duì)于模塊來(lái)講,它的local名字空間和global名字空間是一樣的,也就是說(shuō),模塊對(duì)應(yīng)的PyFrameObject對(duì)象里面的f_locals和f_globals指向的是同一個(gè)PyDictObject對(duì)象。
但是對(duì)于函數(shù)而言,局部名字空間和全局名字空間就不一樣了。調(diào)用locals是獲取自身的局部名字空間,而不同函數(shù)的local名字空間是不同的。但是globals函數(shù)的調(diào)用結(jié)果是一樣的,獲取的都是global名字空間,這也符合函數(shù)內(nèi)找不到某個(gè)變量的時(shí)候會(huì)去找全局變量這一結(jié)論。
所以我們說(shuō)在函數(shù)里面查找一個(gè)變量,查找不到的話會(huì)找全局變量,全局變量再?zèng)]有會(huì)查找內(nèi)置變量。本質(zhì)上就是按照自身的local空間、外層的global空間、內(nèi)置的builtin空間的順序進(jìn)行查找。
因此local空間會(huì)有很多個(gè),因?yàn)槊恳粋€(gè)函數(shù)或者類都有自己的局部作用域,這個(gè)局部作用域就可以稱之為該函數(shù)的local空間;但是global空間則全局唯一,因?yàn)樵撟值浯鎯?chǔ)的是全局變量。無(wú)論你在什么地方,通過(guò)調(diào)用globals函數(shù)拿到的永遠(yuǎn)是全局名字空間,向該空間中添加鍵值對(duì),等價(jià)于創(chuàng)建全局變量。
對(duì)于builtin名字空間,它也是一個(gè)字典。當(dāng)local空間、global空間都沒(méi)有的時(shí)候,會(huì)去builtin空間查找。問(wèn)題來(lái)了,builtin名字空間如何獲取呢?答案是使用builtins模塊,通過(guò)builtins.__dict__即可拿到builtin名字空間。
# 等價(jià)于__builtins__ import builtins #我們調(diào)用list顯然是從內(nèi)置作用域、也就是builtin名字空間中查找的 #但我們只寫list也是可以的 #因?yàn)閘ocal空間、global空間沒(méi)有的話,最終會(huì)從builtin空間中查找 #但如果是builtins.list,那么就不兜圈子了 #表示: "builtin空間,就從你這獲取了" print(builtins.list is list) # True builtins.dict = 123 #將builtin空間的dict改成123 #那么此時(shí)獲取的dict就是123 #因?yàn)槭菑膬?nèi)置作用域中獲取的 print(dict + 456) # 579 str = 123 #如果是str = 123,等價(jià)于創(chuàng)建全局變量str = 123 #顯然影響的是global空間 print(str) # 123 # 但是此時(shí)不影響builtin空間 print(builtins.str) # <class 'str'>
這里提一下Python2當(dāng)中,while 1比while True要快,為什么?
因?yàn)門rue在Python2中不是關(guān)鍵字,所以它是可以作為變量名的。那么Python在執(zhí)行的時(shí)候就要先看local空間和global空間里有沒(méi)有True這個(gè)變量,有的話使用我們定義的,沒(méi)有的話再使用內(nèi)置的True。
而1是一個(gè)常量,直接加載就可以,所以while True多了符號(hào)查找這一過(guò)程。但是在Python3中兩者就等價(jià)了,因?yàn)門rue在Python3中是一個(gè)關(guān)鍵字,也會(huì)直接作為一個(gè)常量來(lái)加載。
eval和exec
記得之前介紹 eval 和 exec 的時(shí)候,我們說(shuō)這兩個(gè)函數(shù)里面還可以接收第二個(gè)參數(shù)和第三個(gè)參數(shù),它們分別表示global名字空間、local名字空間。
# 如果不指定,默認(rèn)當(dāng)前所在的名字空間 # 顯然此時(shí)是全局名字空間 exec("name = '古明地覺(jué)'") print(name) # 古明地覺(jué) # 但是我們也可以指定某個(gè)名字空間 dct = {} # 將 dct 作為全局名字空間 # 這里我們沒(méi)有指定第三個(gè)參數(shù),也就是局部名字空間 # 如果指定了全局名字空間、但沒(méi)有指定局部名字空間 # 那么局部名字空間默認(rèn)和全局名字空間保持一致 exec("name = 'satori'", dct) print(dct["name"]) # satori
至于 eval 也是同理:
dct = {"seq": [1, 2, 3, 4, 5]} try: print(eval("sum(seq)")) except NameError as e: print(e) # name 'seq' is not defined # 告訴我們 seq 沒(méi)有被定義 # 因?yàn)槲覀冃枰獙?dct 作為名字空間 print(eval("sum(seq)", dct)) # 15
所以名字空間本質(zhì)上就是一個(gè)字典,所謂的變量不過(guò)是字典里面的一個(gè) key。為了進(jìn)一步加深印象,
再舉個(gè)模塊的例子:
# 我們自定義一個(gè)模塊吧 # 首先模塊也是一個(gè)對(duì)象,類型為 <class 'module'> # 但是底層沒(méi)有將這個(gè)類暴露給我們,所以需要換一種方式獲取 import sys module = type(sys) # 以上就拿到了模塊的類型對(duì)象,調(diào)用即可得到模塊對(duì)象 my_module = module("自己定義的") print(sys) # <module 'sys' (built-in)> print(my_module) # <module '自己定義的'> # 此時(shí)的 my_module 啥也沒(méi)有,我們?yōu)槠涮泶u加瓦 my_module.__dict__["name"] = "古明地覺(jué)" print(my_module.name) # 古明地覺(jué) # 給模塊設(shè)置屬性,本質(zhì)上也是操作相應(yīng)的屬性字典 # 當(dāng)然獲取屬性也是如此。如果再和exec結(jié)合的話 code = """ age = 16 def foo(): return "我是函數(shù)foo" from functools import reduce """ # 此時(shí)屬性就設(shè)置在了模塊的屬性字典里面 exec(code, my_module.__dict__) print(my_module.age) # 16 print(my_module.foo()) # 我是函數(shù)foo print(my_module.reduce(int.__add__, [1, 2, 3, 4, 5])) # 15
怎么樣,是不是很有趣呢?以上就是本次分享的所有內(nèi)容,想要了解更多歡迎前往公眾號(hào):Python編程學(xué)習(xí)圈,每日干貨分享
到此這篇關(guān)于Python學(xué)習(xí)之名字,作用域,名字空間的文章就介紹到這了,更多相關(guān)Python名字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
更改Ubuntu默認(rèn)python版本的兩種方法python-> Anaconda
當(dāng)你安裝 Debian Linux 時(shí),安裝過(guò)程有可能同時(shí)為你提供多個(gè)可用的 Python 版本,因此系統(tǒng)中會(huì)存在多個(gè) Python 的可執(zhí)行二進(jìn)制文件。一般Ubuntu默認(rèn)的Python版本都為2.x, 如何改變Python的默認(rèn)版本呢?下面來(lái)一起看看吧。2016-12-12Python爬蟲之爬取我愛(ài)我家二手房數(shù)據(jù)
我愛(ài)我家的數(shù)據(jù)相對(duì)來(lái)說(shuō)抓取難度不大,基本無(wú)反爬措施. 但若按照規(guī)則構(gòu)造頁(yè)面鏈接進(jìn)行抓取,會(huì)出現(xiàn)部分頁(yè)面無(wú)法獲取到數(shù)據(jù)的情況.在網(wǎng)上看了幾個(gè)博客,基本上都是較為簡(jiǎn)單的獲取數(shù)據(jù),未解決這個(gè)問(wèn)題,在實(shí)際應(yīng)用中會(huì)出錯(cuò),本文有非常詳細(xì)的代碼示例,需要的朋友可以參考下2021-05-05Python編寫繪圖系統(tǒng)之從文本文件導(dǎo)入數(shù)據(jù)并繪圖
這篇文章主要為大家詳細(xì)介紹了Python如何編寫一個(gè)繪圖系統(tǒng),可以實(shí)現(xiàn)從文本文件導(dǎo)入數(shù)據(jù)并繪圖,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-08-08Python3和pyqt5實(shí)現(xiàn)控件數(shù)據(jù)動(dòng)態(tài)顯示方式
今天小編就為大家分享一篇Python3和pyqt5實(shí)現(xiàn)控件數(shù)據(jù)動(dòng)態(tài)顯示方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-12-12將python文件打包exe獨(dú)立運(yùn)行程序方法詳解
這篇文章主要介紹了將python文件打包exe獨(dú)立運(yùn)行程序方法詳解,需要的朋友可以參考下2020-02-02使用Python實(shí)現(xiàn)ELT統(tǒng)計(jì)多個(gè)服務(wù)器下所有數(shù)據(jù)表信息
這篇文章主要介紹了使用Python實(shí)現(xiàn)ELT統(tǒng)計(jì)多個(gè)服務(wù)器下所有數(shù)據(jù)表信息,ETL,是英文Extract-Transform-Load的縮寫,用來(lái)描述將數(shù)據(jù)從來(lái)源端經(jīng)過(guò)抽取(extract)、轉(zhuǎn)換(transform)、加載(load)至目的端的過(guò)程,需要的朋友可以參考下2023-07-07python利用xpath爬取網(wǎng)上數(shù)據(jù)并存儲(chǔ)到django模型中
這篇文章主要介紹了python利用xpath爬取網(wǎng)上數(shù)據(jù)并存儲(chǔ)到django模型中,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02