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