【Python】第六章 模組

2020-08-09 09:13:10

該文章內容整理自《Python程式設計:從入門到實踐》、《流暢的Python》、以及網上各大部落格

模組

Python 提供了強大的模組(Modules)支援,不僅 Python 標準庫中包含了大量的模組(即標準模組),還有大量的第三方模組,開發者自己也可以開發自定義模組。通過這些強大的模組可以極大地提高開發者的開發效率。模組可以理解爲是對程式碼更高階的封裝,即把能夠實現某一特定功能的程式碼編寫在同一個 .py 檔案中,並將其作爲一個獨立的模組,這樣既可以方便其它程式或指令碼匯入並使用,同時還能有效避免函數名和變數名發生衝突

import

import用於匯入模組,其具體用法爲

  1. import 模組名1 [as 別名1], 模組名2 [as 別名2],…:使用這種語法格式的 import 語句,會匯入指定模組中的所有成員(包括變數、函數、類等)。當需要使用模組中的成員時,需用該模組名(或別名)作爲字首,否則 Python 直譯器會報錯
  2. from 模組名 import 成員名1 [as 別名1],成員名2 [as 別名2],…:使用這種語法格式的 import 語句,只會匯入模組中指定的成員,而不是全部成員。當程式中使用該成員時,無需附加任何字首,直接使用成員名(或別名)即可。也可以匯入指定模組中的所有成員,即使用 form 模組名 import *,但此方式不推薦使用。因爲這種用法在匯入後使用模組中的成員時無需以模組名爲字首,這有可能與本檔案中的已有成員名產生衝突

reload()

預設情況下,模組在第一次被匯入之後,其他的匯入都不再有效。如果此時在另一個視窗中改變並儲存了模組的原始碼檔案,也無法更新該模組。這樣設計的原因在於,匯入是一個開銷很大的操作(匯入必須找到檔案,將其編譯成位元組碼,並且執行程式碼),以至於每個檔案、每個程式執行不能夠重複多於一次
若此時使用import匯入模組後模組發生變化,而需要重新匯入新的模組,則可以通過reload()進行重新載入。重新載入包括最初匯入模組時應用的分析過程和初始化過程。這樣就允許在不退出直譯器的情況下重新載入已更改的Python模組。但是注意

  1. 如果模組在語法上是正確的,但在初始化過程中失敗,則匯入過程不能正確地將模組的名字系結到符號表中。這時,必須在模組能被重新載入之前使用__import__()函數載入該模組
  2. 重新載入的模組不刪除最初舊版本模組在符號表中的登記項
  3. 如果模組利用 from…import… 方式從另一模組匯入物件,reload()函數不重新定義匯入的物件,可利用 import… 形式避免這個問題
  4. 提供類的重新載入模組不影響所提供類的任何已存範例,已存範例將繼續使用原來的方法定義,只有該類的新範例使用新格式
  5. reload()函數希望獲得的參數是一個已經載入了的模組物件的名稱,所以如果在過載之前,需要確保已經成功地匯入了這個模組

在 Python2 中reload()是內建函數,能夠直接使用。但是 Python3 把reload()內建函數移到了imp標準庫模組中,因而使用前需要從imp中匯入

__import__()

import語句結合了兩個操作:先搜尋指定名稱的模組,然後將搜尋結果系結到當前作用域中的名稱。而內建函數__import__()只搜尋指定名稱的模組,並且,import語句執行過程中是呼叫__import__()來完成模組檢索的

import語句屬於靜態匯入,而__import__()函數用於動態載入模組。一般形式如下,它返回匯入的模組,一般會賦值給變數,以便後期使用
__import__(name, globals=None, locals=None, fromlist=(), level=0)
其中

  • name:模組名。一方面,在使用import匯入模組時,模組名中不能含有空格或者是以數位開頭,因爲在import語法中空格代表隔離兩個識別符號,而__import__()函數中的參數name則爲一個字串,因而可支援匯入字串形式名稱的模組。另一方面,這樣也使得可以使用變數作爲匯入模組的模組名,即在執行時動態決定匯入的模組
  • globals:全域性變數集合。一般不用設定,設定時常用 globals()
  • locals:全域性變數集合。一般不用設定,設定時常用 locals()。 globals 和 locals 參數用來確定如何在包的上下文中解讀名稱
  • fromlist:給出了應該從由 name 指定的模組匯入物件或子模組的名稱
  • level:絕對或者相對匯入。 0 (預設值) 意味着僅執行絕對匯入。 level 爲正數值表示相對於模組呼叫__import__() 的目錄,將要搜尋的父目錄層數
# from spam.ham import eggs, sausage as saus
_temp = __import__('spam.ham', globals(), locals(), ['eggs', 'sausage'], 0)
eggs = _temp.eggs
saus = _temp.sausage

但是在一般業務邏輯中不建議直接使用__import__(),而是採用importlib模組的import_module()函數來代替。其一般形式爲
import_module(name, package=None)
此時只需要知道模組名稱就可以。如 import_module(’…mod’, ‘pkg.subpkg’) 將會匯入 pkg.mod

匯入包

import不僅能匯入一個模組,還能夠匯入包。包是一個含__init__.py的資料夾,其中存放了多個模組檔案(這是 Python 2 的規定,而在 Python 3 中__init__.py 對包來說並不是必須的)。雖然包是一個包含多個模組的資料夾,它的本質依然是模組,因此包中也可以包含包。另外,相比模組和包,庫是一個更大的概念,例如在 Python 標準庫中的每個庫都有好多個包,而每個包中都有若幹個模組

若要建立一個包,首先需要新增__init__.py檔案。一般而言,此檔案中無需編寫任何程式碼

因爲包其實本質上還是模組,因此匯入模組的語法同樣也適用於匯入包。所以匯入包的方法有:

  1. import 包名[.模組名 [as 別名]]:此時需要用 包名.模組名 來作爲字首
  2. from 包名 import 模組名 [as 別名]
  3. from 包名.模組名 import 成員名 [as 別名]

但若只想匯入包內的個別模組,一種方法是直接在呼叫包的檔案內使用import逐個匯入,但包內模組較多、目錄複雜時,這種方法較爲複雜。此時就需要編寫__init__.py檔案。__init__.py 不同於其他模組檔案,它的模組名不是__init__,而是它所在的包名。例如,在 settings 包中的 __init__.py 檔案的模組名就是 settings。當目錄中包含__init__.py,並用 import 直接匯入該目錄時,Python會首先執行__init__.py裏面的程式碼。因而可在__init__.py統一匯入。另外,在__init__.py內匯入模組需要匯入完整的路徑名稱,如包目錄爲

└── mypackage
    ├── subpackage_1
    │   ├── test11.py
    ├── subpackage_2
        └── test21.py

則__init__.py爲

# from subpackage_1 import test11 # error
from mypackage.subpackage_1 import test11

也可以在 mypackage 目錄的 __init__.py 檔案匯入對於的子目錄,在子目錄的 __init__.py 中才匯入具體某個模組,使得包內模組匯入更爲精準和便捷

匯入不同路徑檔案

當使用 import 語句匯入模組後,Python 會按照以下順序查詢指定的模組檔案:

  1. 在當前目錄,即當前執行的程式檔案所在目錄下查詢
  2. 到 PYTHONPATH(環境變數)下的每個目錄中查詢
  3. 到 Python 預設的安裝目錄下查詢

這些目錄都儲存在標準模組 sys 的 sys.path 列表變數中,通過此變數可以看到指定程式檔案支援查詢的所有目錄。換句話說,如果要匯入的模組沒有儲存在 sys.path 列表的目錄中,那麼匯入該模組並執行程式時 Python 直譯器就會拋出 ModuleNotFoundError(未找到模組)異常。而解決未找到模組異常的方法有 3 種:

  1. 向 sys.path 中臨時新增模組檔案儲存位置的完整路徑。注意新增時路徑是斜槓還是反斜槓,並且注意新增跳脫字元
import sys
sys.path.append('D:\\python_module')
  1. 輸出 sys.path 變數,並將模組放在 sys.path 變數中已包含的模組載入路徑中。一般情況下,預設將 Python 的擴充套件模組新增在 lib\site-packages 路徑,它專門用於存放 Python 的擴充套件模組和包
  2. 設定 path 系統環境變數。另外,Python 在使用 path 變數時,會先按照系統 path 變數的路徑去查詢,然後再按照使用者 path 變數的路徑去查詢

另外,在 sys 模組和 os 模組中有一些常用的函數:

  • os.path.abspath(__file__):獲取當前檔案的全名
  • os.path.dirname():獲取當前物件的父級目錄
  • sys.path.insert():將當前物件的路徑新增到首位
  • sys.path.append():將當前環境變數新增到環境變數的末尾

雙下劃線屬性

Python 的模組中內建有多個雙下劃線成員,分別爲

  1. __all__屬性:模組檔案內建有__all__屬性,它記錄了模組中的所有成員。同時,__all__屬性還能用於模組匯入時進行準確限制。只有以當執行 from xx import * 時,被匯入模組若修改定義了__all__屬性,則只有__all__列表內指定的屬性、函數、類、模組、包可被匯入;若沒定義,則匯入模組內的所有屬性、函數、類、模組、包。同時,也可以在__init__.py檔案中修改__all__對匯入的模組和子目錄進行準確限制。另外,要檢視匯入模組的成員,還可以使用之前介紹過的dir()函數,前面說過dir()可獲得當前模組的屬性列表。因而可在匯入模組後用dir()函數檢視是否匯入成功,或者用dir(模組名)檢視模組中含有哪些成員
  2. __doc__屬性:記錄函數、類或模組的說明文件。說明文件是在函數、類或模組中的第一行用單引號(’)或雙引號(")括起來的字串內容。該屬性不能被繼承。呼叫help(物件名)函數可輸出__doc__屬性的內容,或者也可以直接存取函數、類或模組的__doc__屬性,如下面 下麪demo.py模組
'demo.__doc__'

def f():
	'f.__doc__'

class c:
	'c.__doc__'

print(demo.__doc__)
print(f.__doc__)
print(c.__doc__)
# 或用help()函數檢視
print(help(demo))
print(help(f))
print(help(c))
  1. __name__屬性:記錄函數、類或模組的名稱。對於模組的__name__變數,當直接執行一個模組時,__name__ 變數的值爲 __main__;而將模組被匯入其他程式中並執行該程式時,處於模組中的 __name__ 變數的值就變成了模組名。因此,如果希望測試函數只有在直接執行模組檔案時才執行,則可在呼叫測試函數時增加判斷,即只有當 __name__ ==’__main__’ 時才呼叫測試函數
  2. __file__屬性:記錄當前的檔案路徑。但當引入包時,其實際上執行的是 __init__.py 檔案,因此這裏包的儲存路徑實際上是__init__.py檔案的儲存路徑

__pycache__

Python程式執行時不需要編譯成二進制程式碼,而直接從原始碼執行程式,簡單來說是,Python直譯器將原始碼轉換爲位元組碼,然後再由直譯器來執行這些位元組碼。直譯器的具體工作爲:
1、完成模組的載入和鏈接
2、將原始碼編譯爲PyCodeObject物件(即位元組碼),寫入記憶體中,供CPU讀取
3、從記憶體中讀取並執行,結束後將PyCodeObject寫回硬碟當中,也就是複製到.pyc或.pyo檔案中,以儲存當前目錄下所有指令碼的位元組碼檔案

之後若再次執行該指令碼,它先檢查本地是否有上述位元組碼檔案和該位元組碼檔案的修改時間是否在其原始檔之後,是就直接執行,否則重複上述步驟。而__pycache__資料夾的意義在於,第一次執行程式碼的時候,Python直譯器已經把編譯的位元組碼放在__pycache__資料夾中,這樣以後再次執行的話,如果被呼叫的模組未發生改變,那就直接跳過編譯這一步,直接去__pycache__資料夾中去執行相關的 *.pyc 檔案,大大縮短了專案執行前的準備時間

另外,爲了提高模組載入的速度,每個模組都會在__pycache__資料夾中放置該模組的預編譯模組,命名爲 module.version.pyc,version 是模組的預編譯版本編碼,一般都包含 Python 的版本號。這種命名規則可以保證不同版本的模組和不同版本的 Python 編譯器的預編譯模組可以共存。預編譯模組也是跨平臺的,所以不同的模組是可以在不同的系統和不同的架構之間共用的
如在匯入模組時,模組所在資料夾將自動生成一個對應的__pycache__\module_name.cpython-36.pyc檔案;而在匯入包時,會在包的目錄下生成一個__pycache__/__init__.cpython-36.pyc 檔案

Python 在兩種情況下不檢查快取。第一種,從命令列中直接載入的模組總是會重新編譯並且結果不儲存。第二種,如果沒有源模組,則不會檢查快取。爲了支援無原始碼的部署方式,應該將預編譯模組放在原始碼資料夾中而不是 __pycache__ 中,並且不要包含原始碼模組

也可以使用 -O 和 -OO 參數來降低預編譯模組的大小。-O 會去除 assert 語句,-OO 會去除 assert 語句和 __doc__ 字串。優化模組的後綴名是 .pyo。但是,.pyo 和 .pyc 檔案的執行速度不會比 .py 檔案快,快的地方在於模組載入的速度。compileall 模組可以用來把某個資料夾的中的所有檔案都編譯成爲 .pyc 或者 .pyo 檔案