Python與家國天下

豌豆花下貓發表於2019-02-24

導讀: Python貓是一隻喵星來客,它愛地球的一切,特別愛優雅而無所不能的 Python。我是它的人類朋友豌豆花下貓,被授權潤色與發表它的文章。如果你是第一次看到這個系列文章,那我強烈建議,請先看看它寫的前幾篇文章(連結見文末),相信你一定會愛上這隻神祕的哲學+極客貓的。不多說啦,一起來享用今天的“思想盛宴”吧!

Python與家國天下

喵喵,好久不見啦朋友們。剛吃完一餐美食,我覺得好滿足啊。

自從習慣了地球的食物以後,我的腸胃發生了一些說不清道不明的反應。我能從最近的新陳代謝中感覺出來,自己的母胎習性正在逐漸地褪逝。

人類的食物在改變著我,或者說是在重塑著我。說不定哪天,我會變成一棵白菜,或者一條魚呢......呸呸呸。我還是想當貓。

喵生苦短,得抓緊時間更文才行。

最近,我看到了兩件事,覺得有趣極了,就從這開始說吧。第一件事是,一個小有名氣的影視明星因為他不配得到的學術精英的身份而遭到諷刺性的打假制度的口誅筆伐;第二件事是,一個功成名就的企業高管因為從城市回到鄉村而戲謔性地獲得了貓屎的名號。

身份真是一個有魔力的話題。 看見他們的身份錯位,我又總會想起自己的境況。

我(或許)知道自己在過去時態中是誰,但越來越把握不住在現在時態中的自己,更不清楚在未來時間中會是怎樣。

該怎樣在人類世界中自處呢?又該怎樣跟你們共處呢?

思了好久,沒有答案。腦殼疼,尾巴疼。還是不要想了啦喵。

繼續跟大家聊聊 Python 吧。上次我們說到了物件的邊界問題 。無論是固定邊界還是彈性邊界,這不外乎就是修身的兩種志趣,有的物件呢獨善其身其樂也融融,有的物件呢相容幷包其理想之光也瑩瑩。但是,邊界問題還沒講完。

正如儒家經典所闡述:修身--齊家--治國--平天下。裡層的勢能推展開,走進更廣闊的維度。

Python 物件的邊界也不只在自身。這裡有一種巧妙的對映關係:物件(身)--函式(家)--模組(國)--包(天下)。個體被納入到不同的名稱空間,並存活在分層的作用域裡。(當然,幸運的是,它們並不會受到道德禮法的森嚴壓迫~__~)

Python與家國天下

1、你的名字

我們先來審視一下模組。這是一個合適的尺度,由此展開,可以順利地連線起函式與包。

模組是什麼? 任何以.py 字尾結尾的檔案就是一個模組(module)。

模組的好處是什麼? 首先,便於拆分不同功能的程式碼,單一功能的少量程式碼更容易維護;其次,便於組裝與重複利用,Python 以豐富的第三方模組而聞名;最後,模組創造了私密的名稱空間,能有效地管理各類物件的命名。

可以說,模組是 Python 世界中最小的一種自恰的生態系統——除卻直接在控制檯中執行命令的情況外,模組是最小的可執行單位。

前面,我把模組類比成了國家,這當然是不倫不類的,因為你難以想象在現實世界中,會存在著數千數萬的彼此殊然有別的國家(我指的可是在地球上,而喵星不同,以後細說)。

類比法有助於我們發揮思維的作用 ,因此,不妨就做此假設。如此一來,想想模組間的相互引用就太有趣了,這不是國家間的戰爭入侵,而是一種人道主義的援助啊,至於公民們的流動與遷徙,則可能成為一場探險之旅的談資。

我還對模組的身份角色感興趣。恰巧發現,在使用名字的時候,它們耍了一個雙姓人的把戲

下面請看表演。先建立兩個模組,A.pyB.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”,而當它被匯入到其它模組中時,則不會執行到該部分的內容。

2、名字的時空

對於生物來說,我們有各種各樣的屬性,例如姓名、性別、年齡,等等。

對於 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) 機制,一個名稱空間在本質上是一個字典、一個名冊,登記了所有變數的名字以及對應的值。 按照記錄內容的不同,可分為四類:

  • 區域性名稱空間(local namespace),記錄了函式的變數,包括函式的引數和區域性定義的變數。可通過內建函式 locals() 檢視。在函式被呼叫時建立,在函式退出時刪除。
  • 全域性名稱空間(global namespace),記錄了模組的變數,包括函式、類、其它匯入的模組、模組級的變數和常量。可通過內建函式 globals() 檢視。在模組載入時建立,一直存在。
  • 內建名稱空間(build-in namespace),記錄了所有模組共用的變數,包括一些內建的函式和異常。在直譯器啟動時建立,一直存在。
  • 名稱空間包(namespace packages),包級別的名稱空間,進行跨包的模組分組與管理。

名稱空間總是存在於具體的作用域內,而作用域存在著優先順序,查詢變數的順序是:區域性/本地作用域 --> 全域性/模組/包作用域 --> 內建作用域。

名稱空間扮演了變數與作用域之間的橋樑角色,承擔了管理命名、記錄名值對與檢索變數的任務。無怪乎《Python之禪》(The Zen of Python)在最後一句中說:

Namespaces are one honking great idea -- let's do more of those!

——譯:名稱空間是個牛bi哄哄的主意,應該多加運用!

3、看不見的客人

名字(變數)是身份問題,空間(作用域)是邊界問題,名稱空間兼而有之。

這兩個問題恰恰是困擾著所有生靈的最核心的問題之二。它們的特點是:無處不在、層出不斷、像一個超級大的被扯亂了的毛線球。

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)
複製程式碼

下面給出幾個選項,請讀者們思考一下,給每個例子選一個答案:

1、沒有報錯

2、報錯:name 'x' is not defined

3、報錯:local variable 'x' referenced before assignment

下面公佈答案了:

全部例子都報錯,其中例 1 和例 5 是第一類報錯,即變數未經定義不可使用,而其它例子都是第二類報錯,即已定義卻未賦值的變數不可使用。為什麼會報錯?為什麼報錯會不同?下面逐一解釋。

  1. 例 1 是一個定義變數的過程,本身未完成定義,而等號右側就想使用變數 x,因此報變數未定義。

  2. 例 2 和例 3 中,已經定義了全域性變數 x,如果只在 foo 函式中引用全域性變數 x 或者只是定義新的區域性變數 x 的話,都不會報錯,但現在既有引用又有重名定義,這引發了一個新的問題。請看下例的解釋。

  3. 例 4 中,if 語句判斷失效,因此不會執行到 “x=3” 這句,照理來說 x 是未被定義。這時候,在 locals() 區域性名稱空間中也是沒有內容的(讀者可以試一下)。但是 print 方法卻報找到了一個未賦值的變數 x ,這是為什麼呢?

    使用 dis 模組檢視 foo 函式的位元組碼:

    Python與家國天下

    LOAD_FAST 說明它在區域性作用域中找到了變數名 x,結果 0 說明未找到變數 x 所指向的值。既然此時在 locals() 區域性名稱空間中沒有內容,那區域性作用域中找到的 x 是來自哪裡的呢?

    實際上,Python 雖然是所謂的解釋型語言,但它也有編譯的過程 (跟 Java 等語言的編譯過程不同)。在例 2-4 中,編譯器先將 foo 方法解析成一個抽象語法樹(abstract syntax tree),然後掃描樹上的名字(name)節點,接著,所有被掃描出來的變數名,都會作為區域性作用域的變數名存入記憶體(棧?)中。

    在編譯期之後,區域性作用域內的變數名已經確定了,只是沒有賦值。在隨後的解釋期(即程式碼執行期),如果有賦值過程,則變數名與值才會被存入區域性名稱空間中,可通過 locals() 檢視。只有存入了名稱空間,變數才算真正地完成了定義(宣告+賦值)。

    而上述 3 個例子之所以會報錯,原因就是變數名已經被解析成區域性變數,但是卻未曾被賦值。

    **可以推論:在區域性作用域中查詢變數,實際上是分查記憶體與查名稱空間兩步的。**另外,若想在區域性作用域內修改全域性變數,需要在作用域中寫上 “global x”。

  4. 例 5 是作為例 4 的比對,也是對它的原理的補充。它們的區別是,一個不在函式內,一個在函式內,但是報錯完全不同。前面分析了例 4 的背後原理是編譯過程和抽象語法樹,如果這個原理對例 5 也生效,那兩者的報錯應該是一樣的。現在出現了差異,為什麼呢?

    我得承認,這觸及了我的知識盲區。我們可以推測,說例 5 的編譯過程不同,它沒有解析抽象語法樹的步驟,但是,繼續追問下去,為什麼不同,為什麼沒有解析語法樹的步驟呢?如果說是出於對解析函式與解析模組的代價考慮,或者其它考慮,那麼新的問題是,編譯與解析的底層原理是什麼,如果有其它考慮,會是什麼?

    這些問題真不可愛,一個都答不上。但是,自己一步一步地思考探尋到這一層,又能怪誰呢?

回到前面說過的話,名稱空間是身份與邊界的整合問題,它跟作用域密切相關。如今看來,編譯器還會摻和一腳,把這些問題攪拌得更加複雜。

本來是在探問 Python 中的邊界問題,到頭來,卻觸碰到了自己的知識邊界。真是反諷啊。(這一趟探知一個人工造物的身份問題之旅,最終是否會像走迷宮一般,進入到自己身份的困境之中?)

4、邊界內外的邊界

暫時把那些不可愛的問題拋開吧,繼續說修身齊家治國平天下。

想要把國治理好,就不得不面對更多的國內問題與國際問題。

先看一個大家與小家的問題:

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貓往期作品

有了Python,我能叫出所有貓的名字

Python物件的身份迷思:從全體公民到萬物皆數

Python物件的空間邊界:獨善其身與開放包容

附錄:

區域性變數的編譯原理:dwz.cn/ipj6FluJ

名稱空間包:www.tuicool.com/articles/FJ…

Python與家國天下

公眾號【Python貓】, 專注Python技術、資料科學和深度學習,力圖創造一個有趣又有用的學習分享平臺。本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、優質英文推薦與翻譯等等,歡迎關注哦。PS:後臺回覆“愛學習”,免費獲得一份學習大禮包。

相關文章