PEP 3147 – PYC 存储库目录
- 作者:
- Barry Warsaw <barry at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2009-12-16
- Python 版本:
- 3.2
- 历史记录:
- 2010-01-30, 2010-02-25, 2010-03-03, 2010-04-12
- 决议:
- Python-Dev 消息
摘要
本 PEP 描述了对 Python 导入机制的扩展,该扩展改进了在多个已安装的不同版本的 Python 解释器之间共享 Python 源代码文件的功能。它允许将多个字节编译文件(.pyc 文件)与 Python 源文件(.py 文件)一起放置在同一个位置。此处描述的扩展也可用于支持不同的 Python 编译缓存,例如由启用 Unladen Swallow 的 CPython (PEP 3146) 生成的 JIT 输出。
背景
CPython 将其源代码编译成“字节码”,出于性能考虑,只要源文件发生更改,它就会将此字节码缓存到文件系统中。这使得加载 Python 模块的速度更快,因为可以绕过编译阶段。当您的源文件是 foo.py
时,CPython 将字节码缓存到源文件旁边的 foo.pyc
文件中。
字节码文件包含两个 32 位大端序数字,后跟经 marshal 处理的 [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
文件并在每次编译源代码时对其进行重写。(标准库不受此影响,因为这些发行版上确实安装了多个版本的 stdlib。)
此外,为了减轻这些发行版中操作系统打包程序的负担,发行版软件包不包含 Python 版本号 [6];它们在系统上安装的所有 Python 版本之间共享。在软件包中放置 Python 版本号将是一场维护噩梦,因为每当向发行版添加或删除新的 Python 版本时,所有软件包(及其依赖项)都必须更新。由于可用软件包的数量众多,因此此工作量不可行。
(PEP 384 已被提议用于解决不同 Python 版本之间第三方扩展模块的二进制兼容性问题。)
由于这些发行版无法共享 pyc 文件,因此已开发出复杂的机制来将生成的 pyc 文件放在非共享位置,而源代码仍然是共享的。例如,基于符号链接的 Debian 方案 python-support [8] 和 python-central [9]。这些方法导致了更加复杂、脆弱、难以理解和分散的策略,用于将 Python 应用程序交付给广泛的用户。可以说,用户从其操作系统供应商处获得 Python 的次数多于从上游 tarball 中获得 Python 的次数。因此,为 CPython 解决此 pyc 共享问题对于此类供应商而言是重中之重。
本 PEP 提出了一种解决此问题的方案。
提案
Python 的导入机制扩展为在每个 Python 包目录内的单个目录中写入和搜索字节码缓存文件。此目录将称为 __pycache__
。
此外,pyc 文件名将包含一个区分其编译所用 Python 版本的魔法字符串(称为“标记”)。这允许为单个 Python 源文件共存多个字节编译缓存文件。
魔法标记由实现定义,但应包含实现名称和版本号缩写,例如 cpython-32
。它在所有版本的 Python 中必须是唯一的,并且每当魔法数增加时,都必须定义一个新的魔法标记。因此,Python 3.2 的示例 pyc
文件是 foo.cpython-32.pyc
。
魔法标记可通过 get_tag()
函数在 imp
模块中获得。这与 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
文件不存在,当 Python 被要求导入 foo 时,它会引发 ImportError
错误。换句话说,除非源文件存在,否则 Python 不会从缓存目录导入 pyc 文件。
情况 4:旧的 pyc 文件和无源代码的导入
当源文件存在于 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
特别是具有字节编译的深入知识,因此这些将更新以了解新的布局。将 -b
标志添加到 compileall
中以写入旧版 .pyc
字节编译文件路径名。
bdist_wininst 和 Windows 安装程序
这些工具还在安装时显式编译模块。如果它们不使用 py_compile
和 compileall
,则还需要修改它们以了解新的布局。
文件扩展名检查
存在一些代码会检查以 .pyc
结尾的文件,并简单地删除最后一个字符以找到匹配的 .py
文件。一旦实施此 PEP,此代码显然会失败。
为了支持此用例,我们将向 imp
包 [17] 添加两个新方法。
imp.cache_from_source(py_path)
->pyc_path
imp.source_from_cache(pyc_path)
->py_path
替代实现可以自由覆盖这些函数,以根据其对本 PEP 的支持返回合理的值。当实现(或生效的 PEP 302 加载程序)由于任何原因无法计算适当的文件名时,这些方法可以返回 None
。它们不应该引发异常。
反向移植
对于早于 3.2(可能还有 2.7)版本的 Python,可以移植此 PEP。但是,在 Python 3.2(可能还有 2.7)中,此行为将默认启用,实际上,它将替换旧行为。移植需要默认支持旧布局。我们建议通过使用名为 $PYTHONENABLECACHEDIR
的环境变量或命令行开关 -Xenablecachedir
来启用此功能,从而支持 PEP 3147。
Makefile 和其他依赖项工具
计算 .pyc
文件依赖项(例如,如果 .pyc
丢失则编译源代码)的 Makefile 和其他工具将必须更新以检查新路径。
替代方案
本节描述了在 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 上的 Bazaar 分支 [22] 中进行跟踪,直到准备好合并到 Python 3.2 中。正在进行中的 diff 也可以查看 [23],并且会在上传新更改时自动更新。
截至 2010 年 4 月 1 日,已打开了一个 Rietveld 代码审查问题 [24](不,这不是愚人节玩笑 :)。
参考文献
[21] importlib:https://docs.pythonlang.cn/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
版权
本文档已置于公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3147.rst
上次修改时间:2023-09-09 17:39:29 GMT