1. 程式人生 > >探究 python import機制、module、package與名字空間

探究 python import機制、module、package與名字空間

在開始之前,先了解一個內建函式dir(),它可以幫助我們分析一些內部的東西,dir()的描述是:

dir(): 函式不帶引數時,返回當前範圍內的變數、方法和定義的型別列表;帶引數時,返回引數的屬性、方法列表。如果引數包含方法__dir__(),該方法將被呼叫。如果引數不包含__dir__(),該方法將最大限度地收集引數資訊。

簡單來說,不帶引數時,會返回當前名字空間的內容(通常是locals名字空間),帶引數時,會返回引數的屬性:


這裡定義了變數a,然後檢視 dir() 的內容,可以看到當前的名字空間中包含了一些系統定義的特殊方法還有我們的a。定義一個類,然後檢視類的屬性,可以看到雖然我們在類中什麼都沒有定義,但是因為繼承了 object 的關係已經有一系列的魔法方法了。OK,我們馬上轉入到 import 機制中。

初始化的動作:

當我們在命令列敲完 python 回車之後,python已經做了很多很多的初始化工作了。我們可以想象,會有內建型別的初始化,不然我們就沒辦法使用這些型別來建立例項了;也會有內建函式的初始化,不然連我們的 dir() 都沒法用了;還會有搜尋路徑的初始化,不然 python 怎麼會知道你的指令碼在哪裡呢。總的來說,python的初始化就是設定直譯器的狀態資訊,其中大部分資訊是通過建立內建module來完成的,舉個例子,我們看看剛才 dir() 裡的__builtins__:

這不就是一個叫做‘builtins’的內建 module 嗎,我們平常用的 import 就是用來載入模組的,只是我們不會刻意的區分內建模組和自定義模組罷了。當然了,一個模組也會有它的屬性,我們 dir 看看:

哦,原來 python 把型別和內建函式初始化之後就放在這個 builtins 模組中,這裡我們就可以找到我們熟悉的型別,還有各種各樣的異常型別,還有內建函式等等。哎剛剛不是說還會初始化路徑嗎,在 locals 名字空間裡也沒有看到類似__path__的東西呀,原來,不是所有的初始化的動作都會暴露在名字空間中的,這樣可以讓名字空間更加乾淨,隱藏一些不需要使用者知道的東西。我們來找找看,或許大家都知道了,python 的預設搜尋路徑可以在 sys 內建模組中找到:

這裡先講一講為什麼需要這個路徑,這個路徑就是我們在安裝 python 的時候,需要我們去設定的環境變數。我們可以看到有site-packages資料夾的路徑,這是放第三方庫的地方,用 setup install 的話會把庫複製到這裡,比方說numpy就在裡面,import numpy就是從這個路徑中搜索出來的,還有dlls資料夾的路徑,通常載入的pyd就在裡面,還有lib等等,執行 import 語句的時候,python 會從預設路徑中搜索,當然也會在指令碼所在的路徑搜尋,如果在這些路徑中都找不到要 import 的模組就會報錯。0.0現在知道 import 失敗為什麼要去看看環境變數有沒有錯了吧。

*這段可以跳過。實際上搜索路徑是一個限制,有時候使用相對路徑 import 的時候會找不到模組,尤其是需要載入位於上層目錄的模組的時候要特別注意,這是因為如果不是在 package 中,使用 import 是嚴格限制搜尋範圍的。另外有一個小技巧,python 在設定搜尋路徑的時候,除了設定上面的預設路徑,只要在預設路徑裡面有 .pth 檔案,會把 .pth 檔案中的路徑也放進去,這裡寫一個 .pth ,指定一個在桌面的路徑,放在 site-packages 資料夾中:

我們重新啟動 python 虛擬機器:

這時預設搜尋路徑就增加了一條了,這和手動 sys.path.append() 的效果是一樣的。這樣做的好處是可以把工程放到另一個地方去而在哪裡啟動虛擬機器都可以 import,缺點是增加預設搜尋路徑會使每次 import 要搜尋的時間變長,所以還是看場景使用。

我們繼續來觀察一下這個 sys 模組有什麼東西:

同樣的定義了很多特殊方法,還有一些命令之類的東西,當然 path 也在裡面,哦原來 ps1,ps2 在這裡可以修改:

在 sys 中有一個屬性 modules 很特別,它恰恰是放所有 modules 的地方,包括它自己,後面我們還會和它打交道:

而在眾多內建 modules 中又有一個很特別,就是我們的__main__ modules,對了就是我們當前執行的指令碼,其實這個指令碼在python 眼中也是一個 module,但是這個module名字不叫指令碼的名字而叫__main__, 所以我們經常會寫的__name__ == '__main__',就是判斷一個指令碼是不是正在執行的指令碼,而凡是通過 import 動態載入的其他指令碼的名字都不會是__main__:

OK,其實 python 的初始化動作要複雜得多,這裡總結一下關於 import 的動作有哪些:首先,將所有內建型別和內建函式初始化然後放到 builtins 模組中,然後建立__main__模組,sys模組等等內建模組全部放在 sys.modules 中,設定預設搜尋路徑放在sys.path 中,當然還會設定每一個 module 的元資訊例如__name__、__doc__等等。

import 機制

有了上面的知識,要了解 import 就簡單得多了。這裡要挑明一個問題,import 的關鍵在於將要 import 的動作載入進記憶體,和以怎麼樣的方式暴露到名字空間中是兩回事。我們之前看到,其實所有的內建 modules 都在 sys.modules 中,但是隻有builtins 暴露在名字空間中,所以雖然 sys、os、imp等等早就在名字空間中,但我們還不能直接使用。

1、import 內建 module。內建 modules 本來就在 sys 中,所以在 python 初始化的時候已經載入進記憶體了,所以剩下的問題只是要把它暴露在名字空間中:


這裡就很清晰了,import 一個內建 module 很輕鬆,只要把它放到名字空間中就可以了,import 的 os 就是 sys.modules 裡的os。

2、import 自定義 module。自定義 module 不在記憶體中,所以沒辦法了只好先載入進記憶體,再暴露到名字空間中:


3、import as 組合。as 的唯一功能就是改變暴露到名字空間的方式,所以說載入到記憶體的也還是一個moudle, 只是到了名字空間就換了個名字而已,換湯不換藥:


在 module 之上,python 還有一種管理名字空間的方式 package,package是一個特別的 module,在 python 眼中也還是一個module。package是以資料夾的方式實現的,一個合法的 package 必須有一個__init__.py,我們來建立一個package,裡面包含了一個__init__檔案和檔案 b:


4、import package。package 是一個特殊的 module,所以也要載入進記憶體,然後暴露到名字空間中(as 也適用):

5、import package 中的 modules。定義了一個 package 之後,會為 package 設定一個專屬的搜尋範圍,這個資訊在__spec__的 submodule_search_locations中:


如果要通過 package 搜尋 module 必須在這個指定的搜尋路徑中,否則會報錯,儘管要 import 的 module 能夠在預設路徑中找到:

再看看記憶體的情況,通過 package import module 之後,除了這個 module 被載入進記憶體以外,連這個 package 本身也會載入進記憶體中(as 也適用):


注意到幾個特殊的情況,首先在記憶體中確實有 package 這個 module,要把 package 也一起載入進記憶體有兩個原因,一個是執行 import package.b 的時候,python 把後面的 package.b 做了遞迴,也就是先 import package,取出 package 指定的搜尋路徑,再 import b,所以 package 是先於 b 被載入的,還有一個原因是做快取,當下一次再 import 這個 package 中的 module 的時候就不需要再次載入了。第二個情況是記憶體中有 package.b 但是沒有 b,這其實很容易理解,如果另外一個 pacakge 中也有 b 這個 module,那 python 要怎麼區分呢,所以使用通過 pacakge 的 import 的 module 必須包含 package名。第三個情況是隻把 package 暴露在名字空間中,這個也很簡單,因為第二個原因每次訪問都只能通過 package,所以就沒必要把 b, 也不能把 b 簡單的暴露出來。

6、form import組合。上面說到了直接 import pacakge 裡的  module 不方便,那可以通過 from 進行精準 import,

很顯然,通過 from 和 as 的工作有點相似,在記憶體中也還是載入了 package 和 package.b,而直接將 b 暴露到名字空間中。

7、import *組合。一般來說,import 一個 module 就是把 moudle 中的屬性(變數、函式、類)打包之後建立一個 module,從一個 module 中 import 一個變數相當於把這個變數暴露在名字空間中:

為了方便起見,可以使用 import * 的組合將一個 module 中所有的變數都暴露在名字空間中:

為了限制一些變數不會通過 import * 組合暴露出去,可以在 module 定義__all__屬性:


8、巢狀import。巢狀 import 意思是在 import 一個 module 的時候,這個 module 也有 import 語句,這其實很常見。其實和上面的幾種情況的差別不大,我們可以猜想 import 之後記憶體中會有兩個 module,而看看這兩個 module 暴露在哪個名字空間中罷了:

總結一下 import 的一些行為。重要的事情說多一遍:import 的關鍵在於將要 import 的動作載入進記憶體,和以怎麼樣的方式暴露到名字空間中是兩回事,import 機制所有的行為都是圍繞這兩個問題來的,我們可以看到 sys.modules 就是所有 modules 住的地方,也是一個緩衝池,在每次 import 之前先檢查在不在換衝池中,如果在就不必再次載入,這是一個很重要的點。至於要不要暴露到名字空間中,怎麼暴露到名字空間中,暴露到哪個名字空間中,就是不同 import 語句要做的事情了。

*這段可以跳過。我們想一個問題,在 python 初始化內建型別和內建函式之後,一直放在 builtins 模組中,那新載入的模組怎麼能夠使用內建型別和內建函式呢?我們發現每次載入完模組之後,這個模組都有一個屬性就叫__builtins__,我們來看看:

奇怪了,雖然確實是有一個__builtins__, 但是這是一個 dict,和那個 builtins 不是一個東西呀!別急,python 的 LEGB 規則告訴我們,除了 locals 和 globals 名字空間以外,還有一個名字空間就是 builtins 名字空間,所有的內建型別和內建函式,肯定都不會再區域性和全域性變數中,唯一的辦法就是搜尋到 builtins 中去,所以本質上 builtins 是一個名字空間,維護一個從符號到變數的 dict,也就是在 module 中的 __builtins__。而 builtins modules 是這個 dict 的包裝,還維護了其他一些元資訊,所以如果仔細觀察,在 modules 中的__builtins__就是從 builtins modules 複製過來的。更進一步,所有的執行緒都是共享這個模組的。

module 的使用與名字空間

為什麼要做包管理,要分開不同的 module ,還有這麼複雜的規則呢?最終都是要更好地劃分名字空間,使得每一個寫 module 的人可以盡情使用自己名字空間中的變數,而不用擔心與其他 module 衝突。python 沒有外部變數這一說,只有 LEGB 規則。來看一個例子:

這個例子中,module1 載入 module2,呼叫其中的函式,但是在 moudle1 和 module2 中都有全域性變數 value,這樣一來列印的會是什麼呢:

答案是10,我們用直覺想,寫 module2 的人所希望的一定是打印出它定義的全域性變數10,而不是先考慮有其他的 module 會有一個 value 和它衝突,因為 python 沒有外部變數這一說,如果是這樣,每個寫模組的程式設計師都必須小心翼翼,這是我們不希望的,顯然 python 包管理就是為了應對這種情況的,為了能夠訪問 module 中的變數、函式、類,都必須通過 module 作為字首以示標識:

再換句話說,其實 module 也是一種名字空間,本質上和函式,和類沒有區別,module 也有自己的屬性,有自己的 locals、globals 空間,難道不覺得訪問 module 的變數和訪問類變數的方式很像嗎?

關於名字空間的理解只能意會了,這裡再解答為什麼 python 能夠實現輸出10 而不輸出 1,這是因為和類、函式相同,當需要進入一個新的名字空間的時候,會建立一個新的棧幀處理這個名字空間當中的位元組碼,而再這之前,locals 和 globals 名字空間會更新(也會複製 builtins 名字空間),所以 module1 呼叫 modules 的函式的時候,實際上 globals 名字空間中的 value 已經被更新成 10 了,所以通過LEGB規則就輕鬆解決不同 modules 的命名衝突了。

這篇文章到這裡就算結束了,博主是根據《python原始碼解析》的內容整理出來的,有興趣的小夥伴也可以閱讀,對原始碼有興趣的小夥幫也可以檢視相關的程式碼。關於名字空間的例子出自書中,python2與python3差別很大,在實現 import 機制上有很大的改動,不過核心沒有變化,這篇文章只是一個黑盒解析,拋磚引玉,如果有錯漏的地方也請大家指出。