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-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 文件时,情况也是如此。

流程图

这是一个描述模块加载方式的流程图。

../_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。它们不应该引发异常。

反向移植

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

Makefile 和其他依赖项工具

计算 .pyc 文件依赖项(例如,如果 .pyc 丢失则编译源代码)的 Makefile 和其他工具将必须更新以检查新路径。

替代方案

本节描述了在 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 上的 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 的早期草稿,并建议简化为将传统的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

上次修改时间:2023-09-09 17:39:29 GMT