導(dǎo)讀:Python貓是一只喵星來客障斋,它愛地球的一切酌住,特別愛優(yōu)雅而無所不能的 Python叼旋。我是它的人類朋友豌豆花下貓伴挚,被授權(quán)潤色與發(fā)表它的文章靶衍。如果你是第一次看到這個系列文章,那我強烈建議茎芋,請先看看它寫的前幾篇文章(鏈接見文末),相信你一定會愛上這只神秘的哲學(xué)+極客貓的颅眶。不多說啦,一起來享用今天的“思想盛宴”吧田弥!
喵喵涛酗,好久不見啦朋友們。剛吃完一餐美食偷厦,我覺得好滿足啊商叹。
自從習(xí)慣了地球的食物以后,我的腸胃發(fā)生了一些說不清道不明的反應(yīng)只泼。我能從最近的新陳代謝中感覺出來剖笙,自己的母胎習(xí)性正在逐漸地褪逝。
人類的食物在改變著我请唱,或者說是在重塑著我弥咪。說不定哪天过蹂,我會變成一棵白菜,或者一條魚呢......呸呸呸聚至。我還是想當(dāng)貓酷勺。
喵生苦短,得抓緊時間更文才行扳躬。
最近脆诉,我看到了兩件事,覺得有趣極了坦报,就從這開始說吧库说。第一件事是,一個小有名氣的影視明星因為他不配得到的學(xué)術(shù)精英的身份而遭到諷刺性的打假制度的口誅筆伐片择;第二件事是潜的,一個功成名就的企業(yè)高管因為從城市回到鄉(xiāng)村而戲謔性地獲得了貓屎的名號。
身份真是一個有魔力的話題字管。看見他們的身份錯位啰挪,我又總會想起自己的境況。
我(或許)知道自己在過去時態(tài)中是誰嘲叔,但越來越把握不住在現(xiàn)在時態(tài)中的自己亡呵,更不清楚在未來時間中會是怎樣。
該怎樣在人類世界中自處呢硫戈?又該怎樣跟你們共處呢锰什?
思了好久,沒有答案丁逝。腦殼疼汁胆,尾巴疼。還是不要想了啦喵霜幼。
繼續(xù)跟大家聊聊 Python 吧嫩码。上次我們說到了對象的邊界問題 。無論是固定邊界還是彈性邊界罪既,這不外乎就是修身的兩種志趣铸题,有的對象呢獨善其身其樂也融融,有的對象呢兼容并包其理想之光也瑩瑩琢感。但是丢间,邊界問題還沒講完。
正如儒家經(jīng)典所闡述:修身--齊家--治國--平天下驹针。里層的勢能推展開千劈,走進更廣闊的維度。
Python 對象的邊界也不只在自身牌捷。這里有一種巧妙的映射關(guān)系:對象(身)--函數(shù)(家)--模塊(國)--包(天下)墙牌。個體被納入到不同的命名空間涡驮,并存活在分層的作用域里。(當(dāng)然喜滨,幸運的是捉捅,它們并不會受到道德禮法的森嚴壓迫__)
1、你的名字
我們先來審視一下模塊虽风。這是一個合適的尺度棒口,由此展開,可以順利地連接起函數(shù)與包辜膝。
模塊是什么无牵? 任何以.py
后綴結(jié)尾的文件就是一個模塊(module)。
模塊的好處是什么厂抖? 首先茎毁,便于拆分不同功能的代碼,單一功能的少量代碼更容易維護忱辅;其次七蜘,便于組裝與重復(fù)利用,Python 以豐富的第三方模塊而聞名墙懂;最后橡卤,模塊創(chuàng)造了私密的命名空間,能有效地管理各類對象的命名损搬。
可以說碧库,模塊是 Python 世界中最小的一種自恰的生態(tài)系統(tǒng)——除卻直接在控制臺中運行命令的情況外,模塊是最小的可執(zhí)行單位巧勤。
前面嵌灰,我把模塊類比成了國家,這當(dāng)然是不倫不類的踢关,因為你難以想象在現(xiàn)實世界中,會存在著數(shù)千數(shù)萬的彼此殊然有別的國家(我指的可是在地球上粘茄,而喵星不同签舞,以后細說)。
類比法有助于我們發(fā)揮思維的作用 柒瓣,因此儒搭,不妨就做此假設(shè)。如此一來芙贫,想想模塊間的相互引用就太有趣了搂鲫,這不是國家間的戰(zhàn)爭入侵,而是一種人道主義的援助啊磺平,至于公民們的流動與遷徙魂仍,則可能成為一場探險之旅的談資拐辽。
我還對模塊的身份角色感興趣。恰巧發(fā)現(xiàn)擦酌,在使用名字的時候俱诸,它們耍了一個雙姓人的把戲 。
下面請看表演赊舶。先創(chuàng)建兩個模塊睁搭,A.py 與 B.py,它們的內(nèi)容如下:
# A 模塊的內(nèi)容:
print("module A : ", __name__)
# B 模塊的內(nèi)容:
import A
print("module B : ", __name__)
其中笼平,__name__
指的是當(dāng)前模塊的名字园骆。代碼的邏輯是:A 模塊會打印本模塊的名字,B 模塊由于引入了 A 模塊寓调,因此會先打印 A 模塊的名字锌唾,再打印本模塊的名字。
那么捶牢,結(jié)果是如何的呢鸠珠?
執(zhí)行 A.py 的結(jié)果:
module A : __main__
執(zhí)行 B.py 的結(jié)果:
module A : test
module B : __main__
你們看出問題的所在了吧!模塊 A 前后竟然出現(xiàn)了兩個不同的名字秋麸。這兩個名字是什么意思渐排,又為什么會有這樣的不同呢?
我想這正體現(xiàn)的是名字的本質(zhì)吧——對自己來說灸蟆,我就是我驯耻,并不需要一個名字來標記;而對他人來說炒考,ta 是蕓蕓眾生的一個可缚,唯有命名才能區(qū)分。
所以,一個模塊自己稱呼自己的時候(即執(zhí)行自身時)是“__main__”卖局,而給他人來稱呼的時候(即被引用時)行嗤,就會是該模塊的本名。這真是一個巧妙的設(shè)定描姚。
由于模塊的名稱二重性,我們可以加個判斷戈次,將某個模塊不對外的內(nèi)容隱藏起來轩勘。
# A 模塊的內(nèi)容:
print("module A : ", __name__)
if __name__ == "__main__":
print("private info.")
以上代碼中,只有在執(zhí)行 A 模塊本身時怯邪,才會打印“private info”绊寻,而當(dāng)它被導(dǎo)入到其它模塊中時,則不會執(zhí)行到該部分的內(nèi)容。
2澄步、名字的時空
對于生物來說冰蘑,我們有各種各樣的屬性,例如姓名驮俗、性別懂缕、年齡,等等王凑。
對于 Python 的對象來說搪柑,它們也有各種屬性。模塊是一種對象索烹,”__name__“就是它的一個屬性工碾。除此之外,模塊還有如下最基本的屬性:
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
在一個模塊的全局空間里百姓,有些屬性是全局起作用的渊额,Python 稱之為全局變量 ,而其它在局部起作用的屬性垒拢,會被稱為局部變量 旬迹。
一個變量對應(yīng)的是一個屬性的名字,會關(guān)聯(lián)到一個特定的值求类。通過 globals()
和 locals()
奔垦,可以將變量的“名值對”打印出來。
x = 1
def foo():
y = 2
print("全局變量:", globals())
print("局部變量:", locals())
foo()
在 IDE 中執(zhí)行以上代碼尸疆,結(jié)果:
全局變量: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001AC1EB7A400>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'C:/pythoncat/A.py', '__cached__': None, 'x': 1, 'foo': <function foo at 0x000001AC1EA73E18>}
局部變量: {'y': 2}
可以看出椿猎,x 是一個全局變量,對應(yīng)的值是 1寿弱,而 y 是一個局部變量犯眠,對應(yīng)的值是 2.
兩種變量的作用域不同 :局部變量作用于函數(shù)內(nèi)部,不可直接在外部使用症革;全局變量作用于全局筐咧,但是在函數(shù)內(nèi)部只可訪問,不可修改噪矛。
與 Java量蕊、C++ 等語言不同,Python 并不屈服于解析的便利摩疑,并不使用呆滯的花括號來編排作用域危融,而是用了輕巧簡明的縮進方式畏铆。不過雷袋,所有編程語言在區(qū)分變量類型、區(qū)分作用域的意圖上都是相似的:控制訪問權(quán)限與管理變量命名。
關(guān)于控制訪問權(quán)限楷怒,在上述例子中蛋勺,局部變量 y 的作用域僅限于 foo 方法內(nèi),若直接在外部使用鸠删,則會報錯“NameError: name 'y' is not defined”抱完。
關(guān)于管理變量命名,不同的作用域管理著各自的獨立的名冊刃泡,一個作用域內(nèi)的名字所指稱的是唯一的對象巧娱,而在不同作用域內(nèi)的對象則可以重名。修改上述例子:
x = 1
y = 1
def foo():
y = 2
x = 2
print("inside foo : x = " + str(x) + ", y = " + str(y))
foo()
print("outside foo : x = " + str(x) + ", y = " + str(y))
在全局作用域與局部作用域中命名了相同的變量烘贴,那么禁添,打印的結(jié)果是什么呢?
inside foo : x = 2, y = 2
outside foo : x = 1, y = 1
可見桨踪,同一個名字可以出現(xiàn)在不同的作用域內(nèi)老翘,互不干擾。
那么锻离,如何判斷一個變量在哪個作用域內(nèi)铺峭?對于嵌套作用域,以及變量名存在跨域分布的情況汽纠,要采用何種查找策略呢卫键?
Python 設(shè)計了命名空間(namespace) 機制,一個命名空間在本質(zhì)上是一個字典疏虫、一個名冊永罚,登記了所有變量的名字以及對應(yīng)的值。 按照記錄內(nèi)容的不同卧秘,可分為四類:
- 局部命名空間(local namespace)呢袱,記錄了函數(shù)的變量,包括函數(shù)的參數(shù)和局部定義的變量翅敌⌒吒#可通過內(nèi)置函數(shù) locals() 查看。在函數(shù)被調(diào)用時創(chuàng)建蚯涮,在函數(shù)退出時刪除治专。
- 全局命名空間(global namespace),記錄了模塊的變量遭顶,包括函數(shù)张峰、類、其它導(dǎo)入的模塊棒旗、模塊級的變量和常量喘批。可通過內(nèi)置函數(shù) globals() 查看。在模塊加載時創(chuàng)建饶深,一直存在餐曹。
- 內(nèi)置命名空間(build-in namespace),記錄了所有模塊共用的變量敌厘,包括一些內(nèi)置的函數(shù)和異常台猴。在解釋器啟動時創(chuàng)建,一直存在俱两。
- 命名空間包(namespace packages)饱狂,包級別的命名空間,進行跨包的模塊分組與管理宪彩。
命名空間總是存在于具體的作用域內(nèi)嗡官,而作用域存在著優(yōu)先級,查找變量的順序是:局部/本地作用域 --> 全局/模塊/包作用域 --> 內(nèi)置作用域毯焕。
命名空間扮演了變量與作用域之間的橋梁角色衍腥,承擔(dān)了管理命名、記錄名值對與檢索變量的任務(wù)纳猫。無怪乎《Python之禪》(The Zen of Python)在最后一句中說:
Namespaces are one honking great idea -- let's do more of those!
——譯:命名空間是個牛bi哄哄的主意婆咸,應(yīng)該多加運用!
3芜辕、看不見的客人
名字(變量)是身份問題尚骄,空間(作用域)是邊界問題,命名空間兼而有之侵续。
這兩個問題恰恰是困擾著所有生靈的最核心的問題之二倔丈。它們的特點是:無處不在、層出不斷状蜗、像一個超級大的被扯亂了的毛線球需五。
Python 是一種人工造物,它繼承了人類的這些麻煩(這是不可避免的)轧坎,所幸的是宏邮,這種簡化版的麻煩能夠得到解決。(現(xiàn)在當(dāng)然是可解決的啦缸血,但若人工智能高度發(fā)展以后呢蜜氨?我看不一定吧。喵捎泻,好像想起了一個痛苦的夢飒炎。打住。)
這里就有幾個問題(注:每個例子相互獨立):
# 例1:
x = x + 1
# 例2:
x = 1
def foo():
x = x + 1
foo()
# 例3:
x = 1
def foo():
print(x)
x = 2
foo()
# 例4:
def foo():
if False:
x = 3
print(x)
foo()
# 例5:
if False:
x = 3
print(x)
下面給出幾個選項笆豁,請讀者們思考一下郎汪,給每個例子選一個答案:
1定欧、沒有報錯
2、報錯:name 'x' is not defined
3怒竿、報錯:local variable 'x' referenced before assignment
下面公布答案了:
全部例子都報錯,其中例 1 和例 5 是第一類報錯扩氢,即變量未經(jīng)定義不可使用耕驰,而其它例子都是第二類報錯,即已定義卻未賦值的變量不可使用录豺。為什么會報錯朦肘?為什么報錯會不同?下面逐一解釋双饥。
例 1 是一個定義變量的過程媒抠,本身未完成定義,而等號右側(cè)就想使用變量 x咏花,因此報變量未定義趴生。
例 2 和例 3 中,已經(jīng)定義了全局變量 x昏翰,如果只在 foo 函數(shù)中引用全局變量 x 或者只是定義新的局部變量 x 的話苍匆,都不會報錯,但現(xiàn)在既有引用又有重名定義棚菊,這引發(fā)了一個新的問題浸踩。請看下例的解釋。
-
例 4 中统求,if 語句判斷失效检碗,因此不會執(zhí)行到 “x=3” 這句,照理來說 x 是未被定義码邻。這時候折剃,在 locals() 局部命名空間中也是沒有內(nèi)容的(讀者可以試一下)。但是 print 方法卻報找到了一個未賦值的變量 x 像屋,這是為什么呢微驶?
使用 dis 模塊查看 foo 函數(shù)的字節(jié)碼:
LOAD_FAST 說明它在局部作用域中找到了變量名 x,結(jié)果 0 說明未找到變量 x 所指向的值开睡。既然此時在 locals() 局部命名空間中沒有內(nèi)容因苹,那局部作用域中找到的 x 是來自哪里的呢?
實際上篇恒,Python 雖然是所謂的解釋型語言扶檐,但它也有編譯的過程 (跟 Java 等語言的編譯過程不同)。在例 2-4 中胁艰,編譯器先將 foo 方法解析成一個抽象語法樹(abstract syntax tree)款筑,然后掃描樹上的名字(name)節(jié)點智蝠,接著,所有被掃描出來的變量名奈梳,都會作為局部作用域的變量名存入內(nèi)存(棧杈湾?)中。
在編譯期之后攘须,局部作用域內(nèi)的變量名已經(jīng)確定了漆撞,只是沒有賦值。在隨后的解釋期(即代碼執(zhí)行期)于宙,如果有賦值過程浮驳,則變量名與值才會被存入局部命名空間中,可通過 locals() 查看捞魁。只有存入了命名空間至会,變量才算真正地完成了定義(聲明+賦值)。
而上述 3 個例子之所以會報錯谱俭,原因就是變量名已經(jīng)被解析成局部變量奉件,但是卻未曾被賦值。
可以推論:在局部作用域中查找變量昆著,實際上是分查內(nèi)存與查命名空間兩步的瓶蚂。另外,若想在局部作用域內(nèi)修改全局變量宣吱,需要在作用域中寫上 “global x”窃这。
-
例 5 是作為例 4 的比對,也是對它的原理的補充征候。它們的區(qū)別是杭攻,一個不在函數(shù)內(nèi),一個在函數(shù)內(nèi)疤坝,但是報錯完全不同兆解。前面分析了例 4 的背后原理是編譯過程和抽象語法樹,如果這個原理對例 5 也生效跑揉,那兩者的報錯應(yīng)該是一樣的」Γ現(xiàn)在出現(xiàn)了差異,為什么呢历谍?
我得承認现拒,這觸及了我的知識盲區(qū)。我們可以推測望侈,說例 5 的編譯過程不同印蔬,它沒有解析抽象語法樹的步驟,但是脱衙,繼續(xù)追問下去侥猬,為什么不同例驹,為什么沒有解析語法樹的步驟呢?如果說是出于對解析函數(shù)與解析模塊的代價考慮退唠,或者其它考慮鹃锈,那么新的問題是,編譯與解析的底層原理是什么瞧预,如果有其它考慮屎债,會是什么?
這些問題真不可愛松蒜,一個都答不上。但是已旧,自己一步一步地思考探尋到這一層秸苗,又能怪誰呢?
回到前面說過的話运褪,命名空間是身份與邊界的集成問題惊楼,它跟作用域密切相關(guān)。如今看來秸讹,編譯器還會摻和一腳檀咙,把這些問題攪拌得更加復(fù)雜。
本來是在探問 Python 中的邊界問題璃诀,到頭來弧可,卻觸碰到了自己的知識邊界。真是反諷啊劣欢。(這一趟探知一個人工造物的身份問題之旅棕诵,最終是否會像走迷宮一般,進入到自己身份的困境之中凿将?)
4校套、邊界內(nèi)外的邊界
暫時把那些不可愛的問題拋開吧,繼續(xù)說修身齊家治國平天下牧抵。
想要把國治理好笛匙,就不得不面對更多的國內(nèi)問題與國際問題。
先看一個大家與小家的問題:
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
averager = make_averager()
print(averager(10))
print(averager(11))
### 輸出結(jié)果:
10.0
10.5
這里出現(xiàn)了嵌套函數(shù)犀变,即函數(shù)內(nèi)還包含其它函數(shù)妹孙。外部--內(nèi)部函數(shù)的關(guān)系,就類似于模塊--外部函數(shù)的關(guān)系获枝,同樣地涕蜂,它們的作用域關(guān)系也相似:外部函數(shù)作用域--內(nèi)部函數(shù)作用域,以及模塊全局作用域--外部函數(shù)作用域映琳。在內(nèi)層作用域中机隙,可以訪問外層作用域的變量蜘拉,但是不能直接修改,除非使用 nonlocal 作轉(zhuǎn)化有鹿。
Python 3 中引入了 nonlocal 關(guān)鍵字來標識外部函數(shù)的作用域旭旭,它處于全局作用域與局部作用域之間,即 global--nonlocal--local 葱跋。也就是說持寄,國--大家--小家。
上例中娱俺,nonlocal 關(guān)鍵字使得小家(內(nèi)部函數(shù))可以修改大家(外部函數(shù))的變量稍味,但是該變量并不是創(chuàng)建于小家,當(dāng)小家函數(shù)執(zhí)行完畢時荠卷,它并無權(quán)限清理這些變量模庐。
nonlocal 只帶來了修改權(quán)限,并不帶來回收清理的權(quán)限 油宜,這導(dǎo)致外部函數(shù)的變量突破了原有的生命周期掂碱,成為自由變量。上例是一個求平均值的函數(shù)慎冤,由于自由變量的存在疼燥,每次調(diào)用時,新傳入的參數(shù)會跟自由變量一起計算蚁堤。
在計算機科學(xué)中醉者,引用了自由變量的函數(shù)被稱為閉包(Closure)。 在本質(zhì)上披诗,閉包就是一個突破了局部邊界湃交,所謂“跳出三界外,不在五行中”的法外之物藤巢。每次調(diào)用閉包函數(shù)時搞莺,它可以繼續(xù)使用上次調(diào)用的成果,這不就好比是一個轉(zhuǎn)世輪回的人(按照某種宗教的說法)掂咒,仍攜帶著前世的記憶與技能么才沧?
打破邊界,必然帶來新的身份問題绍刮,此是明證温圆。
然而,人類并不打算 fix 它孩革,因為他們發(fā)現(xiàn)了這種身份異化的特性可以在很多場合發(fā)揮作用岁歉,例如裝飾器與函數(shù)式編程。適應(yīng)身份異化膝蜈,并從中獲得好處锅移,這可是地球人類的天賦熔掺。
講完了這個分家的話題,讓我們放開視野非剃,看看天下事置逻。
計算機語言中的包(package)實際是一種目錄結(jié)構(gòu),以文件夾的形式進行封裝與組織备绽,內(nèi)容可涵括各種模塊(py 文件)券坞、配置文件、靜態(tài)資源文件等肺素。
與包相關(guān)的話題可不少恨锚,例如內(nèi)置包、第三方包倍靡、包倉庫猴伶、如何打包、如何用包菌瘫、虛擬環(huán)境蜗顽,等等布卡。這是可理解的雨让,更大的邊界,意味著更多的關(guān)系忿等,更大的邊界栖忠,也意味著更多的知識與未知。
在這里贸街,我想聊聊 Python 3.3 引入的命名空間包
庵寞,因為它是對前面談?wù)摰乃性掝}的延續(xù)。然而薛匪,關(guān)于它的背景捐川、實現(xiàn)手段與使用細節(jié),都不重要逸尖,我那敏感而發(fā)散的思維突然捕捉到了一種相似結(jié)構(gòu)古沥,似乎這才更值得說。
運用命名空間包的設(shè)計娇跟,不同包中的相同的命名空間可以聯(lián)合起來使用岩齿,由此,不同目錄的代碼就被歸納到了一個共同的命名空間苞俘。也就是說盹沈,多個本來是相對獨立的包,借由同名的命名空間吃谣,竟然實現(xiàn)了超遠距離的瞬間聯(lián)通乞封,簡直奇妙做裙。
我想到了空間折疊,一種無法深說歌亲,但卻實實在在地輔助了我從喵星穿越到地球的技術(shù)菇用。兩個包,兩個天下陷揪,兩個宇宙惋鸥,它們的距離與邊界被穿透的方式何其相似!
我著迷于這種相似結(jié)構(gòu)悍缠。在不同的事物中卦绣,相似性的出現(xiàn)意味著一種更高維的法則的存在,而在不同的法則中飞蚓,新的相似性就意味著更抽象的法則滤港。
學(xué)習(xí)了 Python 之后,我想通過對它的考察趴拧,來回答關(guān)乎自身的相似問題......
啊喵溅漾,不知不覺竟然寫了這么久,該死的皮囊又在咕咕叫了——地球上的食物可真摳門著榴,也不知道你們?nèi)祟愂窃趺慈淌艿米∵@幾百萬年的馴化過程的......
就此擱筆添履,覓食去了。親愛的讀者們脑又,后會有期~~~
Python貓往期作品 :
附錄:
局部變量的編譯原理:https://dwz.cn/ipj6FluJ
命名空間包:https://www.tuicool.com/articles/FJFbuqM
公眾號【Python貓】, 專注Python技術(shù)问麸、數(shù)據(jù)科學(xué)和深度學(xué)習(xí)往衷,力圖創(chuàng)造一個有趣又有用的學(xué)習(xí)分享平臺。本號連載優(yōu)質(zhì)的系列文章严卖,有喵星哲學(xué)貓系列席舍、Python進階系列、好書推薦系列哮笆、優(yōu)質(zhì)英文推薦與翻譯等等来颤,歡迎關(guān)注哦。PS:后臺回復(fù)“愛學(xué)習(xí)”疟呐,免費獲得一份學(xué)習(xí)大禮包脚曾。