Python基礎手冊28——模組的高階概念

weixin_34320159發表於2018-02-11

三、模組名稱空間

名稱空間(名稱空間)中儲存了變數名到物件的對映。向名稱空間新增變數名的操作過程涉及到繫結變數到指定物件的操作(以及給該物件的引用計數加 1 )。改變一個變數的繫結叫做重新繫結,刪除一個變數叫做解除繫結。

我們在之前已經介紹過在程式執行期間有兩個或三個活動的名稱空間。 這三個名稱空間分別是區域性名稱空間,全域性名稱空間和內建名稱空間,但區域性名稱空間在執行期間是不斷變化的,所以我們說"兩個或三個"。 從名稱空間中訪問這些變數名依賴於它們的載入順序,或是系統載入這些名稱空間的順序。

Python 直譯器首先載入內建名稱空間。 它由 __builtins__ 模組中的名字構成。 隨後載入執行檔案的全域性名稱空間,它會在模組開始執行後變為活動名稱空間。 這樣我們就有了兩個活動的名稱空間。

如果在執行期間呼叫了一個函式,那麼將建立出第三個名稱空間,即區域性名稱空間。 我們可以通過 globals()locals() 內建函式判斷出某一名字屬於哪個名稱空間。


1、名稱空間和變數作用域

名稱空間儲存的是純粹意義上的變數名和物件間的對映關係,而作用域還指出了從使用者程式碼的哪些物理位置可以訪問到這些變數名。
1873530-924e259234f31847.png

注意每個名稱空間都是一個單獨的單元。但從作用域的觀點來看,事情是不同的。所有區域性名稱空間的名稱都在區域性作用範圍內。區域性作用範圍以外的所有變數名都在全域性作用範圍內。

還要記得在程式執行過程中,區域性名稱空間和作用域會隨函式呼叫而不斷變化,而全域性名稱空間是不變的。


2、變數查詢

那麼作用域的規則是如何聯絡到名稱空間的呢? 它所要做的就是變數名查詢。訪問一個變數時,直譯器必須在三個名稱空間中的一個找到它。 首先從區域性名稱空間開始,如果沒有找到,直譯器將繼續查詢全域性名稱空間。如果這也失敗了,它將在內建名稱空間裡查詢。

3、無限制的名稱空間

Python 的一個有用的特性在於你可以在任何需要放置資料的地方獲得一個名稱空間。比如,你可以在任何時候給函式新增屬性(使用熟悉的句點屬性標識)。

4、檔案生成名稱空間

模組最好理解為變數名的封裝,也就是定義想讓系統其餘部分看見變數名的場所。從技術上來講,模組通常相應於檔案,而 Python 會建立模組物件,以包含模組檔案內所賦值的所有變數名。簡而言之,模組就是名稱空間(變數名建立所在的場所),而存在於模組之內的變數名就是模組物件的屬性。

5、匯入和作用域

不匯入一個檔案,就無法存取該檔案內所定義的變數名。也就是說,你不能自動看見另一個檔案內的變數名。對於函式也是一樣的,函式絕對無法看見其他函式內的變數名,除非它們從物理上處於同一個函式內。模組程式程式碼絕對無法看見其他模組內的變數名,除非明確地進行了匯入。

在 Python 中,一段程式的作用域完全由程式所處的檔案中的實際位置決定。作用域絕不會被函式呼叫或模組匯入影響。

直譯器執行到這些匯入語句,如果在搜尋路徑中找到了指定的模組,就會載入它。該過程遵循作用域原則,如果在一個模組的頂層匯入,那麼它的作用域就是全域性的;如果在函式中匯入,那麼它的作用域是區域性的。

6、名稱空間的巢狀

雖然匯入不會使名稱空間發生向上的巢狀,但確實會發生向下的巢狀。利用屬性的點號運算路徑,有可能深入到任意巢狀的模組中並讀取其屬性。

\模組通過使用自包含的變數的包,也就是所謂的名稱空間提供了將功能化的介面組織為系統的簡單的方法。在一個模組檔案的頂層定義的所有的變數名都成了被匯入的模組物件的屬性。匯入給予了對模組的全域性作用域中的變數的讀取權。也就是說,在模組匯入時,模組檔案的全域性作用域變成了模組物件的名稱空間。Python 的模組允許將獨立的檔案連線成一個更大的程式系統。

四、模組搜尋路徑

通常來說,匯入過程最重要的部分是最一開始的搜尋部分,也就是定位要匯入的檔案。在大多數情況下,可以依賴模組匯入搜尋路徑的自動特性,完全不需要配置這些路徑。

Python 的模組搜尋路徑是下面這些部分組合而成的結果:
1、程式的主目錄
2、PYTHONPATH 目錄(如果已經進行了設定)
3、標準連結庫目錄
4、任何 .pth 檔案的內容(如果存在的話)

這四個元件組合起來就變成了 sys.path 。預設搜尋路徑是在編譯或是安裝時指定的。搜尋路徑的第一和第三元素是自動定義的,第二和第四元素,可以用於擴充路徑,從而包含你自己的原始碼目錄。
1873530-fc0c1ac86172d72d.png

當匯入的模組不在搜尋路徑裡時,匯入模組操作會失敗。
1873530-0da3b1ed7fe06969.png


1、主目錄

Python 首先會在主目錄內搜尋匯入的檔案。當你執行一個程式的時候,這個目錄就是你執行程式頂層指令碼檔案時所在的目錄。當在互動模式下工作時,這一入口就是你當前的工作目錄。

因為這個目錄總是先被搜尋,如果程式完全位於單一目錄,所有匯入都會自動工作,而不需要配置路徑。另一方面,由於這個目錄是先搜尋的,其檔案也將覆蓋路徑上的其他目錄中具有同樣名稱的模組。
1873530-b0f0ab880bf855bf.png


2、PYTHONPATH 目錄

搜尋完主目錄之後如果沒有發現相應的檔案,Python會從左至右(假設你設定了的話)搜尋 PYTHONPATH 環境變數設定中羅列出的所有目錄。PYTHONPATH 是設定包含 Python 程式檔案的目錄的列表,這些目錄可以是使用者定義的或平臺特定的目錄名。你可以把想匯入的目錄都加進來,而 Python 會使用你的設定來擴充套件模組搜尋的路徑。

因為 Python 會先搜尋主目錄,當匯入的檔案跨目錄時,這個設定才顯得格外重要。也就是說,如果你需要被匯入的檔案與進行匯入的檔案處在不同目錄時,可以考慮設定這個環境變數。
1873530-c773e6e8acaf51fa.png


3、標準庫目錄

接著,Python 會自動搜尋標準庫模組的安裝目錄。因為這些一定會被搜尋,通常是不需要新增到 PYTHONPATH 之中或包含到路徑檔案中的。

4、.pth 檔案目錄

Python 允許使用者通過配置 .pth 檔案把有效的目錄新增到模組搜尋路徑中去,也就是在字尾名 .pth(路徑的意思)的文字檔案中一行一行地列出目錄。

簡而言之,當內含目錄名稱的文字檔案放到適當目錄時,也可以概括地扮演與PYTHONPATH 環境變數設定相同的角色。

當存在目錄的時候,Python 會把檔案每行所羅列的目錄從頭到尾加到模組搜尋路徑列表的最後。實際上,Python 會將收集它所找到的所有路徑檔案中的目錄名,並且過濾掉任何重複的和不存在的目錄。
1873530-541f4389b6306205.png

搜尋路徑的 PYTHONPATH 和路徑檔案部分允許我們調整匯入查詢檔案的地方。 搜尋路徑的配置可能隨平臺以及 Python 版本而異。取決於你所使用的平臺,附加的目錄也可能自動加入模組搜尋路徑。


5、sys.path 列表

如果你想檢視模組搜尋路徑在機器上的實際配置,可以通過列印內建的 sys.path 列表(也就是標準庫模組 sys 的 path 屬性)來檢視這個路徑。目錄名稱的字串列表就是Python 內部實際的搜尋路徑。匯入時,Python 會由左至右搜尋這個列表。

sys.path 是模組搜尋的路徑。Python 在程式啟動時進行配置,自動將頂級檔案的主目錄(或者指定當前工作目錄的一個空字串)、任何 PYTHONPATH 目錄、標準庫目錄,以及已經建立的任何 .pth 檔案路徑的內容合併。得到一個 Python 在每次匯入一個新檔案的時候查詢的目錄名的字串列表。這個 sys.path 列表可以幫你確認你所做的搜尋路徑的設定值:如果在列表中看不到設定值,就需要重新檢查你的設定。
1873530-70bfd609ff7adba1.png

如果你知道在做什麼,這個列表也提供一種方式,讓指令碼手動調整其搜尋路徑。通過修改 sys.path 這個列表,你可以修改將來的匯入的搜尋路徑。一旦做了這類修改,就會對 Python 程式中將要匯入的地方產生影響,因為所有匯入和檔案都共享了同一個sys.path 列表。因此可以使用這個技巧,在 Python 程式中動態配置搜尋路徑。不過要小心:如果從路徑中刪除了重要目錄,就無法獲取一些關鍵的工具了。

sys.path 的設定方法只在修改的 Python 會話或程式(即程式)中才會存續。在Python 程式結束後,不會保留下來。PYTHONPATH 和 .pth 檔案路徑配置時儲存在作業系統中,而不是執行的 Python 程式中。PYTHONPATH 和 .pth 檔案提供了更改持久的路徑修改方法。因此使用這種配置方法更全域性一些:機器上的每個程式都會去查詢PYTHONPATH 和 .pth,而且在程式結束後,他們還存在著。
1873530-86c8cd817d12df46.png

修改完成後,你就可以載入自己的模組了。 只要這個列表中的某個目錄包含這個檔案, 它就會被正確匯入。 當然, 這個方法是把目錄追加在搜尋路徑的尾部。 如果你有特殊需要, 那麼應該使用列表的 insert() 方法操作 ,把目錄追加在搜尋路徑的首部。


6、模組檔案選擇

檔名的字尾(例如.py)是刻意從 import 語句中省略的。Python 會選擇搜尋路徑中第一個符合匯入檔名的檔案。

  • 原始碼檔案:b.py
  • 位元組碼檔案:b.pyc
  • 目錄:b,包匯入
  • 編譯擴充套件模組(通常是C或C++編寫),匯入時使用動態連線
  • 用 C 編寫的編譯好的內建模組,並通過靜態連線至 Python。
  • ZIP 檔案元件,匯入時會自動解壓縮
  • 記憶體映像,對於 frozen 可執行檔案。

如果在不同目錄中有 b.py 和 b.so,Python 總是在由左至右搜尋 sys.path 時,載入模組搜尋路徑那些目錄中最先出現(最左邊的)相符檔案。但是,如果實在相同的目錄中找到這兩個檔案,Python會遵循一個標準的調訓順序,不過這種順序不保證永遠保持不變,通常來說,你不應該依賴 Python 會在給定的目錄中選擇何種的檔案型別:讓模組名獨特一些,或者設定模組搜尋路徑,讓模組選擇的特性更明確一些。


五、模組的高階概念

1、核心風格: 匯入語句的位置

推薦所有的模組在 Python 模組的開頭部分匯入。 而且最好按照這樣的順序:

  • Python 標準庫模組
  • Python 第三方模組
  • 應用程式自定義模組

然後使用一個空行分割這三類模組的匯入語句。 這將確保模組使用固定的習慣匯入,有助於減少每個模組需要的 import 語句數目。

2、從 ZIP 檔案中匯入模組

在 2.3 版中,Python 加入了從 ZIP 歸檔檔案匯入模組的功能。 如果你的搜尋路徑中存在一個包含 Python 模組(.py, .pyc, or .pyo 檔案)的 .zip 檔案,匯入時會把 ZIP 檔案當作目錄處理,在檔案中搜尋模組。
如果要匯入的一個 ZIP 檔案只包含 .py 檔案, 那麼 Python 不會為其新增對應的 .pyc 檔案,這意味著如果一個 ZIP 歸檔沒有匹配的 .pyc 檔案時,匯入速度會相對慢一點。

3、用字串格式的模組名匯入模組

一條 import 或 from 語句中的模組名是直接編寫的變數名稱。然而,有時候,我們的程式可以在執行時以一個字串的形式獲取要匯入的模組的名稱。但是我們無法使用import 語句來直接載入以字串形式給出其名稱的一個模組,Python 期待一個變數名,而不是字串。
1873530-f65f0feaf1fdff0d.png

為了解決這個問題,我們需要使用特殊的工具,從執行時生成的一個字串來動態地載入一個模組。最通用的方法是,把一條匯入語句構建為 Python 程式碼的一個字串,並且將其傳遞給 exec() 內建函式以執行。
1873530-c3129e5d98273523.png

exec() 函式編譯一個程式碼字串,並且將其傳給 Python 直譯器以執行。exec() 唯一的缺點就是,每次執行時它必須編譯 import 語句。如果需要執行很多次,使用內建的 __import__ 函式來從一個字串名稱載入,程式碼會執行的更快。
1873530-6fd92f85b7ce1b06.png


4、__name__ 和 __main__

每個模組都有個 __name__ 的內建屬性,Python 會自動設定該屬性:

如果檔案是以頂層程式檔案執行,在啟動時,__name__ 就會設定為字串 "__main__" 。

如果檔案被匯入,__name__ 就會被設成模組名。
1873530-bd73195b189c6fdc.png

結果就是模組可以檢測自己的 __name__ 變數來確定它是在執行還是再匯入。使用 __name__ 變數最常見的就是編寫測試程式碼。簡而言之,可以在檔案末尾加個 __name__ 判斷語句,把測試程式碼放到判斷語句中。

編寫既可以作為命名行工具也可以作為工具庫使用的檔案時,__name__ 技巧也很好用。


5、模組設計理念

就像函式一樣,模組也有設計方面的折中考慮:需要思考哪些函式和類要放進模組以及模組通訊機制等。

總是在Python的模組內編寫程式碼:

沒有辦法寫出不在某個模組之內的程式程式碼。事實上,在互動模式提示符下輸入的程式程式碼,其實是存在於內建模組 __main__ 之內。互動模式提示符獨特之處就在於程式是執行後就立刻丟棄,以及表示式結果是自動列印的。

載入模組會導致這個模組被"執行"。 也就是被匯入模組的頂層程式碼將直接被執行。 這通常包括設定全域性變數以及類和函式的宣告。 如果有檢查 __name__ 的操作,那麼它也會被執行。
當然,這樣的執行可能不是我們想要的結果。 你應該把儘可能多的程式碼封裝到函式。 明確地說,只把函式和類定義放入模組的頂層是良好的模組程式設計習慣。

模組耦合要降到最低:

我們應該儘量的最小化模組間的耦合性。

模組應該少去修改其他模組的變數:

使用另一個模組定義的全域性變數,這完全是可以的(畢竟這就是客戶端匯入服務的方式),但是,修改另一個模組內的全域性變數,通常是出現設計問題的徵兆。


6、頂層程式碼語句次序的重要性

當模組首次匯入(或過載)時,Python會從頭到尾的執行語句。在匯入時,模組檔案頂層的程式程式碼(不在函式內)就會被執行。因此,該語句是無法引用檔案後面位置賦值的變數名。位於函式主體內的程式碼直到函式被呼叫後才會執行。因為函式內的變數名在函式實際執行前都不會解析,通常可以引用檔案內任意地方的變數。


《Python基礎手冊》系列:

Python基礎手冊 1 —— Python語言介紹
Python基礎手冊 2 —— Python 環境搭建(Linux)
Python基礎手冊 3 —— Python直譯器
Python基礎手冊 4 —— 文字結構
Python基礎手冊 5 —— 識別符號和關鍵字
Python基礎手冊 6 —— 操作符
Python基礎手冊 7 —— 內建函式
Python基礎手冊 8 —— Python物件
Python基礎手冊 9 —— 數字型別
Python基礎手冊10 —— 序列(字串)
Python基礎手冊11 —— 序列(元組&列表)
Python基礎手冊12 —— 序列(型別操作)
Python基礎手冊13 —— 對映(字典)
Python基礎手冊14 —— 集合
Python基礎手冊15 —— 解析
Python基礎手冊16 —— 檔案
Python基礎手冊17 —— 簡單語句
Python基礎手冊18 —— 複合語句(流程控制語句)
Python基礎手冊19 —— 迭代器
Python基礎手冊20 —— 生成器
Python基礎手冊21 —— 函式的定義
Python基礎手冊22 —— 函式的引數
Python基礎手冊23 —— 函式的呼叫
Python基礎手冊24 —— 函式中變數的作用域
Python基礎手冊25 —— 裝飾器
Python基礎手冊26 —— 錯誤 & 異常
Python基礎手冊27 —— 模組
Python基礎手冊28 —— 模組的高階概念
Python基礎手冊29 —— 包

相關文章