Python 1.5 中內建包支援
Python 1.5 中內建包支援
從 Python 1.5a4 版本開始,包支援已內建到 Python 直譯器中。這實現了一個略微簡化和修改過的包匯入語義版本,該語義由“ni”模組首創。
“包匯入”是一種透過使用“帶點的模組名”來組織 Python 模組名稱空間的方法。例如,模組名 A.B 表示包 A 中的一個名為 B 的子模組。正如使用模組使不同模組的作者不必擔心彼此的全域性變數名一樣,使用帶點的模組名使 NumPy 或 PIL 等多模組包的作者不必擔心彼此的模組名。
從 Python 1.3 版本開始,包匯入由一個標準的 Python 庫模組“ni”支援。(這個名字據說是 New Import 的首字母縮寫,但實際上指的是電影《巨蟒與聖盃》中“說 Ni 的騎士”,在亞瑟王的騎士帶著灌木叢返回後,他們改名為“說 Neeeow ... Wum ... Ping 的騎士”——但那是另一個故事了。)
ni 模組除了對 Python 解析器進行了一些修改(也在 1.3 中引入)以接受“import A.B.C”和“from A.B.C import X”形式的 import 語句之外,全部是使用者程式碼。當 ni 未啟用時,使用此語法會導致執行時錯誤“No such module”。一旦 ni 啟用(透過在匯入其他模組之前執行“import ni”),ni 的匯入鉤子將查詢正確包的子模組。
新的包支援旨在類似於 ni,但已進行了簡化,並且一些功能已更改或刪除。
一個例子
假設您想設計一個用於統一處理聲音檔案和聲音資料的包。存在許多不同的聲音檔案格式(通常透過其副檔名識別,例如 .wav、.aiff、.au),因此您可能需要建立和維護一個不斷增長的模組集合,用於在各種檔案格式之間進行轉換。您還可能希望對聲音資料執行許多不同的操作(例如混音、添加回聲、應用均衡器功能、建立人工立體聲效果),因此您還將編寫源源不斷的模組來執行這些操作。以下是您的包可能的結構(以分層檔案系統的形式表示)
Sound/ Top-level package
__init__.py Initialize the sound package
Utils/ Subpackage for internal use
__init__.py
iobuffer.py
errors.py
...
Formats/ Subpackage for file format conversions
__init__.py
wavread.py
wavwrite.py
aiffread.py
aiffwrite.py
auread.py
auwrite.py
...
Effects/ Subpackage for sound effects
__init__.py
echo.py
surround.py
reverse.py
...
Filters/ Subpackage for filters
__init__.py
equalizer.py
vocoder.py
karaoke.py
dolby.py
...
包的使用者可以從包中匯入單個模組,例如
import Sound.Effects.echo- 這將載入子模組 Sound.Effects.echo。它必須用其完整名稱引用,例如
Sound.Effects.echo.echofilter(input, output, delay=0.7, atten=4) from Sound.Effects import echo- 這也載入了子模組 echo,並使其無需包字首即可使用,因此可以按以下方式使用:
echo.echofilter(input, output, delay=0.7, atten=4) from Sound.Effects.echo import echofilter- 同樣,這會載入子模組 echo,但這會使其函式 echofilter 直接可用:
echofilter(input, output, delay=0.7, atten=4)
請注意,當使用 from package import item 時,item 可以是包的子模組(或子包),也可以是包中定義的其他名稱,如函式、類或變數。import 語句首先測試 item 是否在包中定義;如果沒有,它假定它是一個模組並嘗試載入它。如果找不到,則會引發 ImportError。
相反,當使用諸如 import item.subitem.subsubitem 這樣的語法時,除了最後一個之外的每個 item 都必須是一個包;最後一個 item 可以是一個模組或一個包,但不能是前一個 item 中定義的類、函式或變數。
從包匯入 *;__all__ 屬性
現在,當用戶編寫 from Sound.Effects import * 時會發生什麼?理想情況下,人們希望這會以某種方式訪問檔案系統,查詢包中存在哪些子模組,並全部匯入它們。不幸的是,此操作在 Mac 和 Windows 平臺上執行不佳,因為檔案系統並不總是具有關於檔名字母大小寫的準確資訊!在這些平臺上,無法保證知道檔案 ECHO.PY 應該作為模組 echo、Echo 還是 ECHO 匯入。(例如,Windows 95 有一種令人討厭的做法,即所有檔名都以大寫字母開頭。)DOS 8+3 檔名限制為長模組名增加了另一個有趣的問題。
唯一的解決方案是讓包作者提供包的顯式索引。import 語句使用以下約定:如果包的 __init__.py 程式碼定義了一個名為 __all__ 的列表,則當遇到 from package import * 時,它將被視為應匯入的模組名列表。包作者有責任在釋出新版本包時保持此列表最新。如果包作者認為從其包匯入 * 沒有用處,他們也可以決定不支援它。例如,檔案 Sounds/Effects/__init__.py 可能包含以下程式碼
__all__ = ["echo", "surround", "reverse"]這意味著
from Sound.Effects import * 將匯入 Sound 包的三個命名子模組。如果未定義 __all__,語句 from Sound.Effects import * 不會將包 Sound.Effects 中的所有子模組匯入到當前名稱空間中;它只確保包 Sound.Effects 已匯入(可能執行其初始化程式碼 __init__.py),然後匯入包中定義的任何名稱。這包括 __init__.py 定義的任何名稱(以及顯式載入的子模組)。它還包括先前 import 語句顯式載入的包的任何子模組,例如
在此示例中,echo 和 surround 模組被匯入到當前名稱空間中,因為它們在執行 from...import 語句時在 Sound.Effects 包中定義。(這在定義 __all__ 時也適用。)import Sound.Effects.echo import Sound.Effects.surround from Sound.Effects import *
請注意,通常不鼓勵從模組或包匯入 * 的做法,因為它經常導致程式碼難以閱讀。但是,在互動式會話中為了節省打字是可以使用它的,並且某些模組旨在只匯出遵循特定模式的名稱。
請記住,使用 from Package import specific_submodule 沒有任何問題!事實上,除非匯入模組需要使用來自不同包的同名子模組,否則這會成為推薦的表示法。
包內引用
子模組通常需要相互引用。例如,surround 模組可能會使用 echo 模組。事實上,此類引用非常常見,以至於 import 語句在標準模組搜尋路徑中查詢之前,首先會在包含包中查詢。因此,surround 模組只需使用 import echo 或 from echo import echofilter。如果在當前包(當前模組所屬的包)中找不到匯入的模組,import 語句將查詢具有給定名稱的頂層模組。
當包被組織成子包時(如示例中的 Sound 包),沒有捷徑可以引用兄弟包的子模組——必須使用子包的完整名稱。例如,如果模組 Sound.Filters.vocoder 需要使用 Sound.Effects 包中的 echo 模組,它可以使用 from Sound.Effects import echo。
(人們可以設計一種表示法來引用父包,類似於 Unix 和 Windows 檔案系統中“..”用於引用父目錄的用法。事實上,ni 支援使用 __ 表示包含當前模組的包,__.__ 表示父包等等。此功能因其笨拙而被取消;由於大多數包將具有相對較淺的子結構,因此這不是一個很大的損失。)
細節
包也是模組!
警告:對於熟悉 Java 包表示法(類似於 Python 但有所不同)的人來說,以下內容可能會令人困惑。
每當載入包的子模組時,Python 都會確保包本身首先載入,必要時載入其 __init__.py 檔案。包也是如此。因此,當執行語句 import Sound.Effects.echo 時,它首先確保 Sound 已載入;然後確保 Sound.Effects 已載入;只有在那之後,它才確保 Sound.Effects.echo 已載入(如果之前未載入則載入)。
一旦載入,包和模組之間的區別就微乎其微了。事實上,兩者都由模組物件表示,並且都儲存在已載入模組表 sys.modules 中。sys.modules 中的鍵是模組的完整帶點名稱(這不總是與 import 語句中使用的名稱相同)。這也是 __name__ 變數的內容(它給出模組或包的完整名稱)。
__path__ 變數
包和模組之間的一個區別在於 __path__ 變數的存在與否。這隻存在於包中。它被初始化為一個包含包目錄名(sys.path 上某個目錄的子目錄)的列表,其中只有一個專案。更改 __path__ 會更改搜尋包子模組的目錄列表。例如,Sound.Effects 包可能包含特定於平臺的子模組。它可以使用以下目錄結構
Sound/
__init__.py
Effects/ # Generic versions of effects modules
__init__.py
echo.py
surround.py
reverse.py
...
plat-ix86/ # Intel x86 specific effects modules
echo.py
surround.py
plat-PPC/ # PPC specific effects modules
echo.py
Effects/__init__.py 檔案可以操作其 __path__ 變數,使得適當的特定於平臺的子目錄位於主 Effects 目錄之前,這樣某些效果的特定於平臺的實現(如果可用)會覆蓋通用(可能較慢)的實現。例如
platform = ... # Figure out which platform applies dirname = __path__[0] # Package's main folder __path__.insert(0, os.path.join(dirname, "plat-" + platform))
如果不希望特定於平臺的子模組隱藏具有相同名稱的通用模組,則應使用 __path__.append(...) 而不是 __path__.insert(0, ...)。
請注意,plat-* 子目錄不是 Effects 的子包——檔案 Sound/Effects/plat-PPC/echo.py 對應於模組 Sound.Effects.echo。
sys.modules 中的佔位符條目
在使用包時,您偶爾會在 sys.modules 中發現虛假條目,例如 sys.modules['Sound.Effects.string'] 的值可能為 None。這是一個“間接”條目,因為 Sound.Effects 包中的某個子模組匯入了頂層 string 模組而建立的。它的目的是一個重要的最佳化:因為 import 語句無法判斷需要本地模組還是全域性模組,並且因為規則規定本地模組(在同一個包中)會隱藏同名的全域性模組,所以 import 語句必須在查詢(可能已經匯入的)全域性模組之前搜尋包的搜尋路徑。由於搜尋包的路徑是相對昂貴的操作,而匯入已經匯入的模組應該很便宜(大約一兩次字典查詢),因此需要進行最佳化。當同一個全域性模組被同一個包的子模組第二次匯入時,佔位符條目可以避免搜尋包的路徑。
佔位符條目僅為在頂層找到的模組建立;如果根本找不到模組,匯入將失敗,通常不需要最佳化。此外,在互動式使用中,使用者可以將模組建立為包本地子模組並重試匯入;如果已建立佔位符條目,則不會找到。如果使用者透過建立與包中已使用的全域性模組同名的本地子模組來更改包結構,結果通常被稱為“混亂”,正確的解決方案是退出直譯器並重新開始。
如果我的模組和包同名怎麼辦?
您可能有一個目錄(在 sys.path 上)既有模組 spam.py 又有子目錄 spam,其中包含 __init__.py(如果沒有 __init__.py,目錄將不被識別為包)。在這種情況下,子目錄具有優先權,匯入 spam 將忽略 spam.py 檔案,而是載入包 spam。如果您希望模組 spam.py 具有優先權,則必須將其放置在 sys.path 中更靠前的目錄中。
(提示:搜尋順序由函式 imp.get_suffixes() 返回的字尾列表決定。通常,字尾按以下順序搜尋:“.so”、“module.so”、“.py”、“.pyc”。目錄沒有明確出現在此列表中,但優先於其中的所有條目。)
安裝包的建議
為了讓 Python 程式使用一個包,該包必須能被 import 語句找到。換句話說,該包必須是 sys.path 上某個目錄的子目錄。
傳統上,確保包在 sys.path 上的最簡單方法是將其安裝在標準庫中,或者讓使用者透過設定其 $PYTHONPATH shell 環境變數來擴充套件 sys.path。在實踐中,這兩種解決方案很快就會導致混亂。
專用目錄
在 Python 1.5 中,建立了一個約定,透過賦予系統管理員更多控制權來防止混亂。首先,在預設搜尋路徑的末尾添加了兩個額外的目錄(如果安裝字首和 exec_prefix 不同,則為四個)。這些目錄相對於安裝字首(預設為 /usr/local)
- $prefix/lib/python1.5/site-packages
- $prefix/lib/site-python
site-packages 目錄可用於可能依賴 Python 版本的包(例如,包含共享庫或使用新功能的包)。site-python 目錄用於與 Python 1.4 的向後相容性,以及與所使用的 Python 版本無關的純 Python 包或模組。
這些目錄的推薦用法是將每個包放置在其自己的子目錄中,無論是 site-packages 還是 site-python 目錄。子目錄應為包名,包名應可作為 Python 識別符號。然後,任何 Python 程式都可以透過給出其完整名稱來匯入包中的模組。例如,示例中使用的 Sound 包可以安裝在 $prefix/lib/python1.5/site-packages/Sound 目錄中,以啟用諸如 import Sound.Effects.echo 的匯入語句)。
新增間接層
有些站點希望將他們的包安裝在其他地方,但仍然希望所有使用者執行的所有 Python 程式都能匯入它們。這可以透過兩種不同的方式實現
- 符號連結
- 如果包是為帶點名稱匯入而構建的,請在其頂級目錄的 site-packages 或 site-python 目錄中放置一個符號連結。符號連結的名稱應為包名;例如,Sound 包可以有一個符號連結 $prefix/lib/python1.5/site-packages/Sound,指向 /usr/home/soundguru/lib/Sound-1.1/src。
- 路徑配置檔案
- 如果包確實需要向 sys.path 新增一個或多個目錄(例如,因為它尚未構建為支援帶點名稱匯入),則可以在 site-python 或 site-packages 目錄中放置一個名為 package.pth 的“路徑配置檔案”。此檔案中的每一行(註釋和空行除外)都被視為包含一個目錄名,該目錄名將附加到 sys.path。允許使用相對路徑名,並相對於包含 .pth 檔案的目錄進行解釋。
.pth 檔案按字母順序讀取,區分大小寫與本地檔案系統相同。這意味著,如果您發現無法抗拒地想要玩弄目錄搜尋順序,至少您可以以可預測的方式進行操作。(這並不等同於認可。典型的安裝應該沒有或很少有 .pth 檔案,否則就出問題了;如果您需要玩弄搜尋順序,那麼問題非常大。儘管如此,有時確實需要,這就是您必須這樣做的方式。)
Mac 和 Windows 平臺注意事項
在 Mac 和 Windows 上,約定略有不同。這些平臺上包安裝的常規目錄是 Python 安裝目錄的根目錄(或子目錄),該目錄特定於已安裝的 Python 版本。這也是搜尋路徑配置檔案 (*.pth) 的(唯一)目錄。
標準庫目錄的子目錄
由於 sys.path 上任何目錄的任何子目錄現在都可以隱式地用作包,人們可能會很容易混淆它們是否旨在如此。例如,假設有一個名為 tkinter 的子目錄,其中包含模組 Tkinter.py。是應該寫 import Tkinter 還是 import tkinter.Tkinter?如果 tkinter 子目錄在路徑上,兩者都將有效,但這會造成不必要的混淆。
我建立了一個簡單的命名約定,應該可以消除這種混淆:非包目錄的名稱中必須包含連字元。特別是,所有特定於平臺的子目錄(sunos5、win、mac 等)都已重新命名為帶有“plat-”字首的名稱。特定於尚未轉換為包的可選 Python 元件的子目錄已重新命名為帶有“lib-”字首的名稱。dos_8x3 子目錄已重新命名為 dos-8x3。下表列出了所有重新命名的目錄
| 舊名稱 | 新名稱 |
| tkinter | lib-tk |
| stdwin | lib-stdwin |
| sharedmodules | lib-dynload |
| dos_8x3 | dos-8x3 |
| aix3 | plat-aix3 |
| aix4 | plat-aix4 |
| freebsd2 | plat-freebsd2 |
| generic | plat-generic |
| irix5 | plat-irix5 |
| irix6 | plat-irix6 |
| linux1 | plat-linux1 |
| linux2 | plat-linux2 |
| next3 | plat-next3 |
| sunos4 | plat-sunos4 |
| sunos5 | plat-sunos5 |
| win | plat-win |
| test | test |
請注意,test 子目錄未重新命名。它現在是一個包。要呼叫它,請使用諸如 import test.autotest 的語句。
其他內容
XXX 我還沒有時間寫完以下專案的討論與 ni 的變化
ni 的以下功能並未完全複製。除非您當前正在使用 ni 模組並希望遷移到內建包支援,否則請忽略本節。
刪除了 __domain__
預設情況下,當包 A.B.C 的子模組匯入模組 X 時,ni 會按 A.B.C.X、A.B.X、A.X 和 X 的順序搜尋。這是由包中的 __domain__ 變數定義的,該變數可以設定為要搜尋的包名列表。內建包支援中取消了此功能。相反,搜尋總是先查詢 A.B.C.X,然後查詢 X。(這是對 Python 其他地方成功使用的名稱空間解析的“兩範圍”方法的逆轉。)
刪除了 __
使用 ni,包可以使用特殊的名稱“__”(兩個下劃線)來使用顯式的“相對”模組名。例如,包 A.B.C 中的模組可以透過 __.__.K.module 形式的名稱引用包 A.B.K 中定義的模組。此功能因其有限的用途和較差的可讀性而被取消。
__init__ 不相容的語義
使用 ni,包內的 __init__.py 檔案(如果存在)將作為包的標準子模組匯入。內建包支援則將 __init__.py 檔案載入到包的名稱空間中。這意味著如果包 A 中的 __init__.py 定義了一個名稱 x,則無需進一步操作即可將其引用為 A.x。使用 ni,__init__.py 必須包含 __.x = x 形式的賦值才能達到相同的效果。
此外,新的包支援要求存在 __init__ 模組;在 ni 下,它是可選的。這是 Python 1.5b1 中引入的一項更改;它旨在避免使用常用名稱(如“string”)的目錄無意中隱藏在模組搜尋路徑中稍後出現的有效模組。
希望向後相容 ni 的包可以測試特殊變數 __ 是否存在,例如
# Define a function to be visible at the package level
def f(...): ...
try:
__
except NameError: # new built-in package support
pass
else: # backwards compatibility for ni
__.f = f
