Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 3147 – PYC 仓库目录

作者:
Barry Warsaw <barry at python.org>
状态:
最终版
类型:
标准跟踪
创建日期:
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)的 C Python 生成的 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 的用户比从上游 tar 包获取的用户更多。因此,解决 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 文件和无源导入

当源文件存在于 legacy pyc 文件旁边时,Python 将忽略所有 legacy pyc 文件。换句话说,如果 foo.pyc 文件存在于 foo.py 文件旁边,则在所有情况下都将忽略 pyc 文件。

然而,为了继续支持无源分发,如果源文件缺失,Python 将导入单独的 pyc 文件,如果它位于源文件本应存在的位置。

案例 5:只读文件系统

当源文件位于只读文件系统上,或者 __pycache__ 目录或 pyc 文件无法写入时,所有相同的规则都适用。当 __pycache__ 被写入的权限不允许写入包含 pyc 文件时,情况也是如此。

流程图

这是描述模块如何加载的流程图

../_images/pep-3147-1.png

替代 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__.pypy 文件。例如

>>> 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_compilecompileall,那么它们也必须进行修改才能理解新布局。

文件扩展名检查

存在一些代码,它们检查以 .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。它们不应引发异常。

向后移植

对于早于 Python 3.2(以及可能 2.7)的版本,可以向后移植此 PEP。然而,在 Python 3.2(以及可能 2.7)中,此行为将默认启用,实际上,它将替换旧行为。向后移植需要默认支持旧布局。我们建议通过使用名为 $PYTHONENABLECACHEDIR 的环境变量或命令行开关 -Xenablecachedir 来启用此功能。

Makefiles 和其他依赖工具

Makefiles 和其他计算 .pyc 文件依赖关系的工具(例如,如果 .pyc 缺失则字节编译源文件)将不得不更新以检查新路径。

备选方案

本节描述了在本 PEP 开发过程中考虑并拒绝的一些替代方法或细节。

十六进制魔术标签

__pycache__ 目录中的 pyc 文件在其文件名中包含一个魔术标签。这些是导入器使用的实际魔术数字的助记标签。我们可以使用二进制魔术数字的十六进制表示 [10] 作为唯一标识符。例如,在 Python 3.2 中

>>> from binascii import hexlify
>>> from imp import get_magic
>>> 'foo.{}.pyc'.format(hexlify(get_magic()).decode('ascii'))
'foo.580c0d0a.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],并随着新更改的上传而自动更新。

自 2010 年 4 月 1 日起,已开放 Rietveld 代码审查问题 [24](不,这不是愚人节玩笑:)。

参考资料

[21] importlib:https://docs.pythonlang.cn/3.1/library/importlib.html

鸣谢

Barry Warsaw 最初的想法是使用胖 Python 字节码文件。Martin von Loewis 审查了 PEP 的早期草案,并建议简化为在目录中存储传统的 pycpyo 文件。许多其他人审查了本 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

最后修改时间:2025-02-01 08:55:40 GMT