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 的首字母縮寫,但實際上指的是電影《蒙蒂·派森與聖盃》中的《說尼的騎士》,在亞瑟王的騎士帶回灌木叢後,他們的名字改成了《說尼奧...嗚...平的騎士》- 但那是另一個故事。)
除了對 Python 解析器(也在 1.3 中引入)進行了一些修改以接受“import A.B.C”和“from A.B.C import X”形式的 import 語句之外,ni 模組的所有程式碼都是使用者程式碼。當 ni 未啟用時,使用此語法會導致執行時錯誤“沒有此模組”。一旦啟用 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 的語法時,除了最後一個專案之外,每個專案都必須是一個包;最後一個專案可以是模組或包,但不能是前一個專案中定義的類或函式或變數。
從包中匯入 *;__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,又有一個包含 __init__.py 的子目錄 spam(如果沒有 __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 中,建立了一個應該可以防止混亂的約定,讓系統管理員擁有更多的控制權。首先,在預設搜尋路徑的末尾添加了兩個額外的目錄(如果安裝字首和執行字首不同,則為四個)。這些目錄相對於安裝字首(預設為 /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