PEP 3147 – PYC 存放目錄
- 作者:
- Barry Warsaw <barry at python.org>
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 建立日期:
- 2009 年 12 月 16 日
- Python 版本:
- 3.2
- 公告歷史:
- 2010 年 1 月 30 日、2010 年 2 月 25 日、2010 年 3 月 3 日、2010 年 4 月 12 日
- 決議:
- Python-Dev 訊息
摘要
本 PEP 描述了 Python 匯入機制的一項擴展,它改進了在多個已安裝的不同版本 Python 直譯器之間共享 Python 原始碼檔案的能力。它透過允許一個以上的位元組編譯檔案 (.pyc 檔案) 與 Python 原始碼檔案 (.py 檔案) 共置來實現此目的。此處描述的擴展也可用於支援不同的 Python 編譯快取,例如可能由啟用 Unladen Swallow (PEP 3146) 的 CPython 產生的 JIT 輸出。
背景
CPython 將其原始碼編譯成「位元組碼」,基於效能考量,每當原始碼檔案有變更時,它會將此位元組碼快取在檔案系統上。這使得 Python 模組的載入速度更快,因為可以跳過編譯階段。當您的原始碼檔案是 foo.py 時,CPython 會將位元組碼快取在原始碼旁邊的 foo.pyc 檔案中。
位元組碼檔案包含兩個 32 位元大端序數字,接著是封送處理的 [2] 程式碼物件。這兩個 32 位元數字分別代表一個魔術數字和一個時間戳記。每當 Python 更改位元組碼格式時,例如透過向其虛擬機器增加新的位元組碼時,魔術數字就會改變。這確保了為先前 VM 版本構建的 pyc 檔案不會引起問題。時間戳記用於確保 pyc 檔案與用於建立它的 py 檔案匹配。當魔術數字或時間戳記不匹配時,py 檔案會被重新編譯並寫入新的 pyc 檔案。
實際上,眾所周知,pyc 檔案在 Python 主要版本之間不相容。閱讀 Python 原始碼中的 import.c [3] 證明,在近期的記憶中,每個新的 CPython 主要版本都提高了 pyc 魔術數字。
原理
Ubuntu [4] 和 Debian [5] 等 Linux 發行版同時為其使用者提供多個 Python 版本。例如,Ubuntu 9.10 Karmic Koala 使用者可以安裝 Python 2.5、2.6 和 3.1,其中 Python 2.6 是預設版本。
這對系統安裝的第三方 Python 原始碼檔案造成了衝突,因為您無法一次為多個 Python 版本編譯單一 Python 原始碼檔案。當 Python 找到一個魔術數字不匹配的 pyc 檔案時,它會退回到較慢的重新編譯原始碼的過程。因此,如果您的系統安裝了 /usr/share/python/foo.py,兩個不同版本的 Python 將會爭奪 pyc 檔案,並在每次編譯原始碼時重新寫入它。(標準函式庫不受此影響,因為在此類發行版上安裝了多個版本的標準函式庫..)
此外,為了減輕這些發行版作業系統封裝人員的負擔,發行版套件不包含 Python 版本號 [6];它們在系統上安裝的所有 Python 版本之間共享。在套件中加入 Python 版本號將是一個維護惡夢,因為每當發行版中增加或刪除新的 Python 版本時,所有套件——以及它們的依賴項——都必須更新。由於可用的套件數量龐大,這項工作是不可行的。
(PEP 384 已被提出以解決第三方擴充模組在不同 Python 版本之間的二進位相容性問題。)
由於這些發行版無法共享 pyc 檔案,因此已開發出精密的機制,將產生的 pyc 檔案放置在非共享位置,而原始碼仍然共享。範例包括基於符號連結的 Debian 方案 python-support [8] 和 python-central [9]。這些方法使得為廣泛使用者提供 Python 應用程式的策略更加複雜、脆弱、難以理解和零碎。可以說,從作業系統供應商而不是上游壓縮檔中獲取 Python 的使用者更多。因此,為 CPython 解決這個 pyc 共享問題是此類供應商的當務之急。
本 PEP 提出了此問題的解決方案。
提案
Python 的匯入機制得到擴展,以在每個 Python 套件目錄內的一個單獨目錄中寫入和搜尋位元組碼快取檔案。此目錄將稱為 __pycache__。
此外,pyc 檔案名稱將包含一個魔術字串(稱為「標籤」),用於區分它們所編譯的 Python 版本。這允許多個位元組編譯快取檔案共存於一個 Python 原始碼檔案中。
魔術標籤由實作定義,但應包含實作名稱和版本號縮寫,例如 cpython-32。它在所有 Python 版本中必須是唯一的,並且每當魔術數字增加時,都必須定義一個新的魔術標籤。Python 3.2 的一個範例 pyc 檔案因此是 foo.cpython-32.pyc。
魔術標籤可透過 imp 模組中的 get_tag() 函式取得。這與 imp.get_magic() 函式平行。
此方案還有一個額外的好處,即減少了 Python 套件目錄中的雜亂。
當 Python 原始碼檔案首次被匯入時,如果套件目錄中尚不存在 __pycache__ 目錄,則會建立一個。匯入原始碼的 pyc 檔案將以魔術標籤格式的名稱寫入 __pycache__ 目錄。如果建立 __pycache__ 目錄或其中的 pyc 檔案失敗,匯入仍會成功,就像在 PEP 3147 之前的世界一樣。
如果 py 原始碼檔案遺失,則 __pycache__ 內的 pyc 檔案將被忽略。這消除了意外匯入過時 pyc 檔案的問題。
為了向後相容性,Python 仍將支援僅 pyc 的發行版,但僅當 pyc 檔案位於 py 檔案 *應存在* 的目錄中時,即不在 __pycache__ 目錄中。只有在 py 原始碼檔案遺失時,才會匯入 __pycache__ 外部的 pyc 檔案。
諸如 py_compile [15] 和 compileall [16] 等工具將會擴展,以自動建立 PEP 3147 格式的版面配置,但會有一個選項來建立僅 pyc 的發行版版面配置。
範例
這在實踐中會是什麼樣子?
假設我們有一個名為 alpha 的 Python 套件,其中包含一個名為 beta 的子套件。位元組編譯之前的原始碼目錄結構可能如下所示
alpha/
__init__.py
one.py
two.py
beta/
__init__.py
three.py
four.py
使用 Python 3.2 位元組編譯此套件後,您會看到以下版面配置
alpha/
__pycache__/
__init__.cpython-32.pyc
one.cpython-32.pyc
two.cpython-32.pyc
__init__.py
one.py
two.py
beta/
__pycache__/
__init__.cpython-32.pyc
three.cpython-32.pyc
four.cpython-32.pyc
__init__.py
three.py
four.py
注意:列表順序可能因平台而異。
假設安裝了兩個新版本的 Python,一個是 Python 3.3,另一個是 Unladen Swallow。位元組編譯後,檔案系統將如下所示
alpha/
__pycache__/
__init__.cpython-32.pyc
__init__.cpython-33.pyc
__init__.unladen-10.pyc
one.cpython-32.pyc
one.cpython-33.pyc
one.unladen-10.pyc
two.cpython-32.pyc
two.cpython-33.pyc
two.unladen-10.pyc
__init__.py
one.py
two.py
beta/
__pycache__/
__init__.cpython-32.pyc
__init__.cpython-33.pyc
__init__.unladen-10.pyc
three.cpython-32.pyc
three.cpython-33.pyc
three.unladen-10.pyc
four.cpython-32.pyc
four.cpython-33.pyc
four.unladen-10.pyc
__init__.py
three.py
four.py
如您所見,只要 Python 版本識別字串是唯一的,任意數量的 pyc 檔案都可以共存。這些識別字串在下面有更詳細的描述。
這種版面配置的一個很好的特性是 __pycache__ 目錄通常可以被忽略,這樣一個正常的目錄列表會顯示如下:
alpha/
__pycache__/
__init__.py
one.py
two.py
beta/
__pycache__/
__init__.py
three.py
four.py
這比現今的 Python 更不雜亂。
Python 行為
當 Python 搜尋要匯入的模組(例如 foo)時,它可能會遇到幾種情況。根據當前的 Python 規則,「匹配的 pyc」意味著魔術數字與當前直譯器的魔術數字匹配,並且原始碼檔案的時間戳記與 pyc 檔案中的時間戳記完全匹配。
案例 0:穩態
當 Python 被要求匯入模組 foo 時,它會沿著其 sys.path 搜尋 foo.py 檔案(或 foo 套件,但這對本次討論並不重要)。如果找到,Python 會查看是否存在匹配的 __pycache__/foo.<magic>.pyc 檔案,如果存在,則載入該 pyc 檔案。
案例 1:首次匯入
當 Python 定位到 foo.py 時,如果 __pycache__/foo.<magic>.pyc 檔案遺失,Python 將建立它,並在必要時建立 __pycache__ 目錄。Python 將解析並位元組編譯 foo.py 檔案,並將位元組碼儲存在 __pycache__/foo.<magic>.pyc 中。
案例 2:第二次匯入
當 Python 被要求第二次匯入模組 foo 時(當然是在不同的程序中),它將再次沿著其 sys.path 搜尋 foo.py 檔案。當 Python 定位到 foo.py 檔案時,它會尋找匹配的 __pycache__/foo.<magic>.pyc,找到後,它會讀取位元組碼並照常繼續。
案例 3:`__pycache__/foo.<magic>.pyc` 無原始碼
有時 foo.py 檔案可能因某種原因被移除,而快取的 pyc 檔案仍保留在檔案系統上。如果 __pycache__/foo.<magic>.pyc 檔案存在,但用於建立它的 foo.py 檔案不存在,當要求匯入 foo 時,Python 將拋出 ImportError。換句話說,除非原始碼檔案存在,否則 Python 不會從快取目錄匯入 pyc 檔案。
案例 4:舊版 pyc 檔案與無原始碼匯入
當原始碼檔案存在於其旁邊時,Python 將忽略所有舊版 pyc 檔案。換句話說,如果 foo.pyc 檔案與 foo.py 檔案並存,則該 pyc 檔案在所有情況下都將被忽略。
然而,為了繼續支援無原始碼發行版,如果原始碼檔案遺失,Python 將匯入一個獨立的 pyc 檔案,只要它位於原始碼檔案本應存在的位置。
案例 5:唯讀檔案系統
當原始碼位於唯讀檔案系統上,或者 __pycache__ 目錄或 pyc 檔案無法寫入時,所有相同的規則都適用。當 __pycache__ 被寫入時,其權限不允許寫入其中包含的 pyc 檔案,情況亦是如此。
流程圖
以下是描述模組如何載入的流程圖
其他 Python 實作
Jython [11]、IronPython [12]、PyPy [13]、Pynie [14] 和 Unladen Swallow 等其他 Python 實作也可以使用 __pycache__ 目錄來儲存對其平台有意義的任何編譯產物。例如,Jython 可以將模組的類別檔案儲存在 __pycache__/foo.jython-32.class 中。
實作策略
此功能針對 Python 3.2,解決了這些版本和所有未來版本的相關問題。它可能會被向後移植到 Python 2.7。供應商可以根據需要自由地將這些變更向後移植到更早的發行版。對於此功能向後移植到 Python 2,當使用 -U 旗標時,可以寫入諸如 foo.cpython-27u.pyc 的檔案。
對現有程式碼的影響
採用此 PEP 將影響 Python 內部和外部的現有程式碼和慣用語法。本節列舉了其中一些影響。
偵測 PEP 3147 可用性
檢測您的 Python 版本是否提供 PEP 3147 功能的最簡單方法是執行以下檢查
>>> import imp
>>> has3147 = hasattr(imp, 'get_tag')
__file__
在 Python 3 中,當您匯入模組時,其 __file__ 屬性指向其原始碼 py 檔案(在 Python 2 中,它指向 pyc 檔案)。套件的 __file__ 指向其 __init__.py 的 py 檔案。例如
>>> import foo
>>> foo.__file__
'foo.py'
# baz is a package
>>> import baz
>>> baz.__file__
'baz/__init__.py'
本 PEP 中的任何內容都不會改變 __file__ 的語義。
本 PEP 提議為模組添加一個 __cached__ 屬性,它將始終指向實際讀取或寫入的 pyc 檔案。當環境變數 $PYTHONDONTWRITEBYTECODE 設定,或給定 -B 選項,或如果原始碼位於唯讀檔案系統上時,__cached__ 屬性將指向 pyc 檔案 *原本會* 被寫入的位置(如果它不存在的話)。這個位置當然包括其路徑中的 __pycache__ 子目錄。
對於不支援 pyc 檔案的其他 Python 實作,__cached__ 屬性可能指向任何有意義的資訊。例如,在 Jython 上,這可能是模組的 .class 檔案:__pycache__/foo.jython-32.class。某些實作可能會使用多個編譯檔案來建立模組,在這種情況下,__cached__ 可能是一個元組。`__cached__` 的確切內容是 Python 實作特定的。
建議在無法計算出任何有意義的值時,實作應將 __cached__ 屬性設定為 None。
py_compile 與 compileall
Python 帶有兩個模組:py_compile [15] 和 compileall [16],它們支援在內建匯入機制之外編譯 Python 模組。py_compile 特別對位元組編譯有深入了解,因此這些模組將被更新以理解新的版面配置。`compileall` 增加了 -b 旗標,用於寫入舊版 .pyc 位元組編譯檔案路徑名稱。
bdist_wininst 與 Windows 安裝程式
這些工具在安裝時也會明確編譯模組。如果它們不使用 py_compile 和 compileall,那麼它們也必須進行修改以理解新的版面配置。
檔案副檔名檢查
存在一些程式碼,它們檢查以 .pyc 結尾的檔案,並簡單地砍掉最後一個字元以找到匹配的 .py 檔案。一旦實現了此 PEP,此程式碼顯然會失敗。
為了支援此使用案例,我們將向 imp 套件 [17] 增加兩個新方法
imp.cache_from_source(py_path)->pyc_pathimp.source_from_cache(pyc_path)->py_path
其他實作可以自由覆寫這些函式,以根據它們對此 PEP 的支援返回合理的值。當實作(或生效的 PEP 302 載入器)因任何原因無法計算出適當的檔案名稱時,這些方法可以返回 None。它們不應拋出例外。
向後移植
對於早於 Python 3.2 (可能還有 2.7) 的版本,可以向後移植此 PEP。然而,在 Python 3.2 (可能還有 2.7) 中,此行為將預設啟用,事實上,它將取代舊行為。向後移植將需要預設支援舊版面配置。我們建議透過使用名為 $PYTHONENABLECACHEDIR 的環境變數或命令列開關 -Xenablecachedir 來啟用此功能,以支援 PEP 3147。
Makefiles 與其他依賴工具
Makefiles 和其他計算 .pyc 檔案依賴項的工具(例如,如果 .pyc 遺失則位元組編譯原始碼)將必須更新以檢查新路徑。
替代方案
本節描述了在 PEP 開發過程中考慮過但被拒絕的一些替代方法或細節。
PEP 304
本 PEP 的目標與已撤回的 PEP 304 的目標之間存在一些重疊。然而 PEP 304 將允許使用者建立一個影子檔案系統層級結構來儲存 pyc 檔案。這種用於 pyc 檔案的影子層級結構概念可以用來實現本 PEP 的目標。儘管 PEP 304 沒有說明為何被撤回,但影子目錄存在許多問題。影子 pyc 檔案的位置不容易被發現,並且將取決於系統和最終使用者對 $PYTHONBYTECODE 環境變數的正確且一致使用。還存在全局影響,這意味著雖然系統可能希望影子 pyc 檔案,但使用者可能不希望這樣做,然而該 PEP 只定義了全有或全無的方法。
作為問題的一個例子,一個常見(儘管脆弱)的 Python 慣用語是這樣定位資料檔案:
from os import dirname, join
import foo.bar
data_file = join(dirname(foo.bar.__file__), 'my.dat')
這將會產生問題,因為 foo.bar.__file__ 將給出影子目錄中 pyc 檔案的位置,並且可能無法從那裡找到相對於原始碼目錄的 my.dat 檔案。
胖位元組編譯檔案
本 PEP 的早期版本描述了「胖」Python 位元組碼檔案。這些檔案將在單一 pyf 檔案中包含多個 pyc 檔案的等效內容,並帶有一個以適當魔術數字為鍵的查詢表。這是一種可擴展的檔案格式,以便可以相當有效地支援前 5 個平行 Python 實作,但同時提供可擴展的查詢表以根據需要擴展 pyf 位元組碼物件。
胖位元組編譯檔案相當複雜,並且本質上引入了困難的競爭條件,因此建議採用當前使用目錄的簡化方案。同樣的問題也適用於使用 zip 檔案作為胖 pyc 檔案格式。
多個檔案副檔名
PEP 作者還考慮了一種方法,即多個瘦位元組編譯檔案位於同一位置,但使用不同的檔案副檔名來指定 Python 版本。例如:foo.pyc25、foo.pyc26、foo.pyc31 等。這被拒絕了,因為寫入如此多不同檔案會造成雜亂。多重副檔名方法使得更新任何依賴於檔案副檔名的工具變得更加困難(並且是一項持續性任務)。
.pyc
有人提出將 __pycache__ 目錄命名為 .pyc 或其他點檔案名稱。這將在 *nix 系統上產生隱藏目錄的效果。BDFL [20] 拒絕此方案的原因有很多,其中包括點檔案僅在某些平台上特殊,而且我們實際上 *不* 希望完全對使用者隱藏這些檔案。
參考實作
此程式碼的工作正在 Launchpad [22] 上的 Bazaar 分支中追蹤,直到準備好合併到 Python 3.2 為止。進行中的差異也可以查看 [23],並隨著新變更的上傳自動更新。
一個 Rietveld 程式碼審查問題 [24] 已於 2010 年 4 月 1 日開啟(不,這不是愚人節笑話 :))。
參考文獻
[21] importlib:https://docs.python.club.tw/3.1/library/importlib.html
致謝
Barry Warsaw 最初的想法是針對胖 Python 位元組碼檔案。Martin von Loewis 審查了 PEP 的早期草稿,並建議簡化為將傳統的 pyc 和 pyo 檔案儲存在目錄中。許多其他人也審查了本 PEP 的早期版本,並提供了有用的回饋,包括但不限於:
- David Malcolm
- Josselin Mouette
- Matthias Klose
- Michael Hudson
- Michael Vogt
- Piotr Ożarowski
- Scott Kitterman
- Toshio Kuratomi
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-3147.rst