導讀: Python貓是一隻喵星來客,它愛地球的一切,特別愛優雅而無所不能的 Python。我是它的人類朋友豌豆花下貓,被受權潤色與發表它的文章。若是你是第一次看到這個系列文章,那我強烈建議,請先看看它寫的前幾篇文章(連接見文末),相信你必定會愛上這隻神祕的哲學+極客貓的。很少說啦,一塊兒來享用今天的「思想盛宴」吧!python
喵喵,很久不見啦朋友們。剛吃完一餐美食,我以爲好知足啊。編程
自從習慣了地球的食物之後,個人腸胃發生了一些說不清道不明的反應。我能從最近的新陳代謝中感受出來,本身的母胎習性正在逐漸地褪逝。跨域
人類的食物在改變着我,或者說是在重塑着我。說不定哪天,我會變成一棵白菜,或者一條魚呢......呸呸呸。我仍是想當貓。bash
喵生苦短,得抓緊時間更文才行。閉包
最近,我看到了兩件事,以爲有趣極了,就從這開始說吧。第一件事是,一個小有名氣的影視明星由於他不配獲得的學術精英的身份而遭到諷刺性的打假制度的口誅筆伐;第二件事是,一個功成名就的企業高管由於從城市回到鄉村而戲謔性地得到了貓屎的名號。編程語言
身份真是一個有魔力的話題。 看見他們的身份錯位,我又總會想起本身的境況。ide
我(或許)知道本身在過去時態中是誰,但愈來愈把握不住在如今時態中的本身,更不清楚在將來時間中會是怎樣。函數式編程
該怎樣在人類世界中自處呢?又該怎樣跟大家共處呢?函數
思了很久,沒有答案。腦袋疼,尾巴疼。仍是不要想了啦喵。學習
繼續跟你們聊聊 Python 吧。上次咱們說到了對象的邊界問題 。不管是固定邊界仍是彈性邊界,這不外乎就是修身的兩種志趣,有的對象呢獨善其身其樂也融融,有的對象呢兼容幷包其理想之光也瑩瑩。可是,邊界問題還沒講完。
正如儒家經典所闡述:修身--齊家--治國--平天下。裏層的勢能推展開,走進更廣闊的維度。
Python 對象的邊界也不僅在自身。這裏有一種巧妙的映射關係:對象(身)--函數(家)--模塊(國)--包(天下)。個體被歸入到不一樣的命名空間,並存活在分層的做用域裏。(固然,幸運的是,它們並不會受到道德禮法的森嚴壓迫~__~)
咱們先來審視一下模塊。這是一個合適的尺度,由此展開,能夠順利地鏈接起函數與包。
模塊是什麼? 任何以.py
後綴結尾的文件就是一個模塊(module)。
模塊的好處是什麼? 首先,便於拆分不一樣功能的代碼,單一功能的少許代碼更容易維護;其次,便於組裝與重複利用,Python 以豐富的第三方模塊而聞名;最後,模塊創造了私密的命名空間,能有效地管理各種對象的命名。
能夠說,模塊是 Python 世界中最小的一種自恰的生態系統——除卻直接在控制檯中運行命令的狀況外,模塊是最小的可執行單位。
前面,我把模塊類比成了國家,這固然是不三不四的,由於你不可思議在現實世界中,會存在着數千數萬的彼此殊然有別的國家(我指的但是在地球上,而喵星不一樣,之後細說)。
類比法有助於咱們發揮思惟的做用 ,所以,不妨就作此假設。如此一來,想一想模塊間的相互引用就太有趣了,這不是國家間的戰爭入侵,而是一種人道主義的援助啊,至於公民們的流動與遷徙,則可能成爲一場探險之旅的談資。
我還對模塊的身份角色感興趣。恰巧發現,在使用名字的時候,它們耍了一個雙姓人的把戲 。
下面請看表演。先建立兩個模塊,A.py 與 B.py,它們的內容以下:
# A 模塊的內容:
print("module A : ", __name__)
# B 模塊的內容:
import A
print("module B : ", __name__)
複製代碼
其中,__name__
指的是當前模塊的名字。代碼的邏輯是:A 模塊會打印本模塊的名字,B 模塊因爲引入了 A 模塊,所以會先打印 A 模塊的名字,再打印本模塊的名字。
那麼,結果是如何的呢?
執行 A.py 的結果:
module A : __main__
執行 B.py 的結果:
module A : test module B : __main__
大家看出問題的所在了吧!模塊 A 先後居然出現了兩個不一樣的名字。這兩個名字是什麼意思,又爲何會有這樣的不一樣呢?
我想這正體現的是名字的本質吧——對本身來講,我就是我,並不須要一個名字來標記;而對他人來講,ta 是芸芸衆生的一個,惟有命名才能區分。
因此,一個模塊本身稱呼本身的時候(即執行自身時)是「__main__」,而給他人來稱呼的時候(即被引用時),就會是該模塊的本名。這真是一個巧妙的設定。
因爲模塊的名稱二重性,咱們能夠加個判斷,將某個模塊不對外的內容隱藏起來。
# A 模塊的內容:
print("module A : ", __name__)
if __name__ == "__main__":
print("private info.")
複製代碼
以上代碼中,只有在執行 A 模塊自己時,纔會打印「private info」,而當它被導入到其它模塊中時,則不會執行到該部分的內容。
對於生物來講,咱們有各類各樣的屬性,例如姓名、性別、年齡,等等。
對於 Python 的對象來講,它們也有各類屬性。模塊是一種對象,」__name__「就是它的一個屬性。除此以外,模塊還有以下最基本的屬性:
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
複製代碼
在一個模塊的全局空間裏,有些屬性是全局起做用的,Python 稱之爲全局變量 ,而其它在局部起做用的屬性,會被稱爲局部變量 。
一個變量對應的是一個屬性的名字,會關聯到一個特定的值。經過 globals()
和 locals()
,能夠將變量的「名值對」打印出來。
x = 1
def foo():
y = 2
print("全局變量:", globals())
print("局部變量:", locals())
foo()
複製代碼
在 IDE 中執行以上代碼,結果:
全局變量: {'__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 是一個全局變量,對應的值是 1,而 y 是一個局部變量,對應的值是 2.
兩種變量的做用域不一樣 :局部變量做用於函數內部,不可直接在外部使用;全局變量做用於全局,可是在函數內部只可訪問,不可修改。
與 Java、C++ 等語言不一樣,Python 並不屈服於解析的便利,並不使用呆滯的花括號來編排做用域,而是用了輕巧簡明的縮進方式。不過,全部編程語言在區分變量類型、區分做用域的意圖上都是類似的:控制訪問權限與管理變量命名。
關於控制訪問權限,在上述例子中,局部變量 y 的做用域僅限於 foo 方法內,若直接在外部使用,則會報錯「NameError: name 'y' is not defined」。
關於管理變量命名,不一樣的做用域管理着各自的獨立的名冊,一個做用域內的名字所指稱的是惟一的對象,而在不一樣做用域內的對象則能夠重名。修改上述例子:
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))
複製代碼
在全局做用域與局部做用域中命名了相同的變量,那麼,打印的結果是什麼呢?
inside foo : x = 2, y = 2 outside foo : x = 1, y = 1
可見,同一個名字能夠出如今不一樣的做用域內,互不干擾。
那麼,如何判斷一個變量在哪一個做用域內?對於嵌套做用域,以及變量名存在跨域分佈的狀況,要採用何種查找策略呢?
Python 設計了命名空間(namespace) 機制,一個命名空間在本質上是一個字典、一個名冊,登記了全部變量的名字以及對應的值。 按照記錄內容的不一樣,可分爲四類:
命名空間老是存在於具體的做用域內,而做用域存在着優先級,查找變量的順序是:局部/本地做用域 --> 全局/模塊/包做用域 --> 內置做用域。
命名空間扮演了變量與做用域之間的橋樑角色,承擔了管理命名、記錄名值對與檢索變量的任務。無怪乎《Python之禪》(The Zen of Python)在最後一句中說:
Namespaces are one honking great idea -- let's do more of those!
——譯:命名空間是個牛bi哄哄的主意,應該多加運用!
名字(變量)是身份問題,空間(做用域)是邊界問題,命名空間兼而有之。
這兩個問題偏偏是困擾着全部生靈的最核心的問題之二。它們的特色是:無處不在、層出不斷、像一個超級大的被扯亂了的毛線球。
Python 是一種人工造物,它繼承了人類的這些麻煩(這是不可避免的),所幸的是,這種簡化版的麻煩可以獲得解決。(如今固然是可解決的啦,但若人工智能高度發展之後呢?我看不必定吧。喵,好像想起了一個痛苦的夢。打住。)
這裏就有幾個問題(注:每一個例子相互獨立):
# 例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)
複製代碼
下面給出幾個選項,請讀者們思考一下,給每一個例子選一個答案:
一、沒有報錯
二、報錯:name 'x' is not defined
三、報錯:local variable 'x' referenced before assignment
下面公佈答案了:
所有例子都報錯,其中例 1 和例 5 是第一類報錯,即變量未經定義不可以使用,而其它例子都是第二類報錯,即已定義卻未賦值的變量不可以使用。爲何會報錯?爲何報錯會不一樣?下面逐一解釋。
例 1 是一個定義變量的過程,自己未完成定義,而等號右側就想使用變量 x,所以報變量未定義。
例 2 和例 3 中,已經定義了全局變量 x,若是隻在 foo 函數中引用全局變量 x 或者只是定義新的局部變量 x 的話,都不會報錯,但如今既有引用又有重名定義,這引起了一個新的問題。請看下例的解釋。
例 4 中,if 語句判斷失效,所以不會執行到 「x=3」 這句,照理來講 x 是未被定義。這時候,在 locals() 局部命名空間中也是沒有內容的(讀者能夠試一下)。可是 print 方法卻報找到了一個未賦值的變量 x ,這是爲何呢?
使用 dis 模塊查看 foo 函數的字節碼:
LOAD_FAST 說明它在局部做用域中找到了變量名 x,結果 0 說明未找到變量 x 所指向的值。既然此時在 locals() 局部命名空間中沒有內容,那局部做用域中找到的 x 是來自哪裏的呢?
實際上,Python 雖然是所謂的解釋型語言,但它也有編譯的過程 (跟 Java 等語言的編譯過程不一樣)。在例 2-4 中,編譯器先將 foo 方法解析成一個抽象語法樹(abstract syntax tree),而後掃描樹上的名字(name)節點,接着,全部被掃描出來的變量名,都會做爲局部做用域的變量名存入內存(棧?)中。
在編譯期以後,局部做用域內的變量名已經肯定了,只是沒有賦值。在隨後的解釋期(即代碼執行期),若是有賦值過程,則變量名與值纔會被存入局部命名空間中,可經過 locals() 查看。只有存入了命名空間,變量纔算真正地完成了定義(聲明+賦值)。
而上述 3 個例子之因此會報錯,緣由就是變量名已經被解析成局部變量,可是卻不曾被賦值。
**能夠推論:在局部做用域中查找變量,其實是分查內存與查命名空間兩步的。**另外,若想在局部做用域內修改全局變量,須要在做用域中寫上 「global x」。
例 5 是做爲例 4 的比對,也是對它的原理的補充。它們的區別是,一個不在函數內,一個在函數內,可是報錯徹底不一樣。前面分析了例 4 的背後原理是編譯過程和抽象語法樹,若是這個原理對例 5 也生效,那二者的報錯應該是同樣的。如今出現了差別,爲何呢?
我得認可,這觸及了個人知識盲區。咱們能夠推測,說例 5 的編譯過程不一樣,它沒有解析抽象語法樹的步驟,可是,繼續追問下去,爲何不一樣,爲何沒有解析語法樹的步驟呢?若是說是出於對解析函數與解析模塊的代價考慮,或者其它考慮,那麼新的問題是,編譯與解析的底層原理是什麼,若是有其它考慮,會是什麼?
這些問題真不可愛,一個都答不上。可是,本身一步一步地思考探尋到這一層,又能怪誰呢?
回到前面說過的話,命名空間是身份與邊界的集成問題,它跟做用域密切相關。現在看來,編譯器還會摻和一腳,把這些問題攪拌得更加複雜。
原本是在探問 Python 中的邊界問題,到頭來,卻觸碰到了本身的知識邊界。真是反諷啊。(這一趟探知一我的工造物的身份問題之旅,最終是否會像走迷宮通常,進入到本身身份的困境之中?)
暫時把那些不可愛的問題拋開吧,繼續說修身齊家治國平天下。
想要把國治理好,就不得不面對更多的國內問題與國際問題。
先看一個你們與小家的問題:
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))
### 輸出結果:
10.0
10.5
複製代碼
這裏出現了嵌套函數,即函數內還包含其它函數。外部--內部函數的關係,就相似於模塊--外部函數的關係,一樣地,它們的做用域關係也類似:外部函數做用域--內部函數做用域,以及模塊全局做用域--外部函數做用域。在內層做用域中,能夠訪問外層做用域的變量,可是不能直接修改,除非使用 nonlocal 做轉化。
Python 3 中引入了 nonlocal 關鍵字來標識外部函數的做用域,它處於全局做用域與局部做用域之間,即 global--nonlocal--local 。也就是說,國--你們--小家。
上例中,nonlocal 關鍵字使得小家(內部函數)能夠修改你們(外部函數)的變量,可是該變量並非建立於小家,當小家函數執行完畢時,它並沒有權限清理這些變量。
nonlocal 只帶來了修改權限,並不帶來回收清理的權限 ,這致使外部函數的變量突破了原有的生命週期,成爲自由變量。上例是一個求平均值的函數,因爲自由變量的存在,每次調用時,新傳入的參數會跟自由變量一塊兒計算。
在計算機科學中,引用了自由變量的函數被稱爲閉包(Closure)。 在本質上,閉包就是一個突破了局部邊界,所謂「跳出三界外,不在五行中」的法外之物。每次調用閉包函數時,它能夠繼續使用上次調用的成果,這不就比如是一個轉世輪迴的人(按照某種宗教的說法),仍攜帶着前世的記憶與技能麼?
打破邊界,必然帶來新的身份問題,此是明證。
然而,人類並不打算 fix 它,由於他們發現了這種身份異化的特性能夠在不少場合發揮做用,例如裝飾器與函數式編程。適應身份異化,並從中得到好處,這但是地球人類的天賦。
講完了這個分家的話題,讓咱們放開視野,看看天下事。
計算機語言中的包(package)實際是一種目錄結構,以文件夾的形式進行封裝與組織,內容可涵括各類模塊(py 文件)、配置文件、靜態資源文件等。
與包相關的話題可很多,例如內置包、第三方包、包倉庫、如何打包、如何用包、虛擬環境,等等。這是可理解的,更大的邊界,意味着更多的關係,更大的邊界,也意味着更多的知識與未知。
在這裏,我想聊聊 Python 3.3 引入的命名空間包
,由於它是對前面談論的全部話題的延續。然而,關於它的背景、實現手段與使用細節,都不重要,我那敏感而發散的思惟忽然捕捉到了一種類似結構,彷佛這才更值得說。
運用命名空間包的設計,不一樣包中的相同的命名空間能夠聯合起來使用,由此,不一樣目錄的代碼就被概括到了一個共同的命名空間。也就是說,多個原本是相對獨立的包,藉由同名的命名空間,居然實現了超遠距離的瞬間聯通,簡直奇妙。
我想到了空間摺疊,一種沒法深說,但卻實實在在地輔助了我從喵星穿越到地球的技術。兩個包,兩個天下,兩個宇宙,它們的距離與邊界被穿透的方式何其類似!
我着迷於這種類似結構。在不一樣的事物中,類似性的出現意味着一種更高維的法則的存在,而在不一樣的法則中,新的類似性就意味着更抽象的法則。
學習了 Python 以後,我想經過對它的考察,來回答關乎自身的類似問題......
啊喵,不知不覺居然寫了這麼久,該死的皮囊又在咕咕叫了——地球上的食物可真摳門,也不知道大家人類是怎麼忍受得住這幾百萬年的馴化過程的......
就此擱筆,覓食去了。親愛的讀者們,後會有期~~~
Python貓往期做品 :
附錄:
局部變量的編譯原理:dwz.cn/ipj6FluJ
命名空間包:www.tuicool.com/articles/FJ…
公衆號【Python貓】, 專一Python技術、數據科學和深度學習,力圖創造一個有趣又有用的學習分享平臺。本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、優質英文推薦與翻譯等等,歡迎關注哦。PS:後臺回覆「愛學習」,免費得到一份學習大禮包。