PEP 488 – 移除 PYO 文件
- 作者:
- Brett Cannon <brett at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建时间:
- 2015 年 2 月 20 日
- Python 版本:
- 3.5
- 历史记录:
- 2015 年 3 月 6 日、2015 年 3 月 13 日、2015 年 3 月 20 日
摘要
本 PEP 提案从 Python 中移除 PYO 文件的概念。为了继续支持根据优化级别分离字节码文件,本 PEP 提案建议在应用优化时,扩展 PYC 文件名以在字节码库目录中包含优化级别。
理由
截至目前,字节码文件有两种形式:PYC 和 PYO。PYC 文件是在解释器启动时未指定优化级别(即未指定 -O
)时生成和读取的字节码文件。PYO 文件表示在指定 **任何** 优化级别(即指定 -O
**或** -OO
)时读取/写入的字节码文件。这意味着,虽然 PYC 文件清楚地界定了生成它们时使用的优化级别(即除了 peepholer 之外没有优化),但 PYO 文件并非如此。用优化级别和文件扩展名来表达:
- 0:
.pyc
- 1 (
-O
):.pyo
- 2 (
-OO
):.pyo
将 .pyo
文件扩展名用于 1 级和 2 级优化意味着无法清楚地判断生成字节码文件时使用了什么优化级别。在读取 PYO 文件时,如果用户没有仔细确保所有 PYO 文件都是使用相同的优化级别生成的(通常通过盲目删除所有 PYO 文件,然后使用 compileall
模块编译所有新的 PYO 文件 [1] 完成),这会导致解释器使用混合的优化级别运行其代码。当人们对 Python 代码进行超出解释器原生支持的优化时,例如使用 astoptimizer 项目 [2],这个问题会更加严重。
在写入 PYO 文件时,每次更改要使用的优化级别或不确定上次生成 PYO 文件时使用了什么优化级别时,都需要删除所有 PYO 文件,这会导致不必要的文件 churn。本 PEP 提案的更改还允许为字节码文件预先编译 **所有** 优化级别,这在当前情况下是不可能的,因为 .pyo
文件扩展名被用于多个优化级别。
至于分发仅包含字节码的模块,对于代码混淆和较小的文件部署的常见用例,分发 .pyc
和 .pyo
文件都是不必要的。这意味着仅包含字节码的模块将只从其非优化的 .pyc
文件名加载。
提案
为了消除 PYO 文件带来的歧义,本 PEP 提案建议消除 PYO 文件及其伴随的 .pyo
文件扩展名。为了使优化级别变得明确,并避免在 __pycache__
目录中不必要地重新生成优化的字节码文件,用于生成字节码文件的优化级别将被纳入字节码文件名。当未指定优化级别时,将使用 PEP 之前的 .pyc
文件名(即文件名中不会指定优化级别)。例如,在 CPython 3.5 中,名为 foo.py
的源文件可能具有以下字节码文件,具体取决于解释器的优化级别(无、-O
和 -OO
):
- 0:
foo.cpython-35.pyc
(即没有变化) - 1:
foo.cpython-35.opt-1.pyc
- 2:
foo.cpython-35.opt-2.pyc
目前,字节码文件名由 importlib.util.cache_from_source()
创建,大致使用 PEP 3147 [3]、[4] 定义的以下表达式:
'{name}.{cache_tag}.pyc'.format(name=module_name,
cache_tag=sys.implementation.cache_tag)
本 PEP 提案建议在指定优化级别时更改表达式:
'{name}.{cache_tag}.opt-{optimization}.pyc'.format(
name=module_name,
cache_tag=sys.implementation.cache_tag,
optimization=str(sys.flags.optimize))
选择 “opt-” 前缀是为了提供一个与缓存标记的视觉分隔符。选择将优化级别放在缓存标记之后是为了保留基于模块名和缓存标记的字节码文件名的词典顺序,这对于单个解释器来说是不会变化的。“opt-” 前缀被选择而不是 “o”,这样可以自我说明。“opt-” 前缀被选择而不是 “O”,是为了避免在优化级别以 “0” 开头时造成混淆。
选择句号而不是连字符作为分隔符,是为了明确区分优化级别不是缓存标记中指定的解释器版本的一部分。它也借鉴了使用句号在文件名中划分语义上不同的概念。
例如,如果向解释器传递了 -OO
,那么文件名将是 importlib.cpython-35.opt-2.pyc
,而不是 importlib.cpython-35.pyo
。
在未应用优化级别时省略新的 opt-
标记应提高向后兼容性。这对于没有使用优化级别的 Python 实现(例如 PyPy [10])来说也更理解。
需要注意的是,这种更改不会影响导入的性能。由于导入系统已经根据解释器的优化级别查找单个字节码文件,如果文件不存在则生成一个新的字节码文件,因此在 __pycache__
目录中引入更多可能的字节码文件不会对 stat 调用产生任何影响。解释器将继续根据优化级别只查找一个字节码文件,因此不会增加 stat 调用次数。
本 PEP 提案的唯一潜在负面结果是可能增加 .pyc
文件的数量,从而增加存储使用量。但对于存储空间有限的平台,可以使用 sys.dont_write_bytecode
关闭字节码生成,以便能够离线控制。
实现
本 PEP 提案的实现可在此处获得 [11]。
importlib
由于 importlib.util.cache_from_source()
是公开字节码文件路径的 API,也是 importlib 直接使用的 API,因此它需要进行最重要的更改。在 Python 3.4 及更高版本中,该函数的签名为:
importlib.util.cache_from_source(path, debug_override=None)
本 PEP 提案建议在 Python 3.5 中将签名更改为:
importlib.util.cache_from_source(path, debug_override=None, *, optimization=None)
引入的 optimization
关键字参数将控制在文件名中指定什么优化级别。如果参数为 None
,则将假定解释器的当前优化级别(包括无优化)。传递给 optimization
的任何参数都将传递给 str()
,并且必须满足 str.isalnum()
为真,否则将引发 ValueError
(这将防止在文件名中使用无效字符)。如果为 optimization
传递空字符串,则将省略优化级别的添加,恢复到本 PEP 提案之前的文件名格式。
预计除了 Python 自己的两个优化级别之外,第三方代码将使用优化名称的哈希值来指定优化级别,例如 hashlib.sha256(','.join(['no dead code', 'const folding'])).hexdigest()
。虽然这可能会导致很长的文件名,但假定大多数用户永远不会查看 __pycache__ 目录的内容,因此这不会是一个问题。
debug_override
参数将被弃用。False
值将等效于 optimization=1
,而 True
值将表示 optimization=''
(None
参数将继续与 optimization
相同)。当 debug_override
被赋予非 None
值时,将引发弃用警告,但目前没有计划完全删除该参数(但最迟将在 Python 4 中删除)。
与字节码文件后缀相关的 importlib.machinery 的各种模块属性将被更新 [7]。DEBUG_BYTECODE_SUFFIXES
和 OPTIMIZED_BYTECODE_SUFFIXES
将被记录为已弃用,并设置为与 BYTECODE_SUFFIXES
相同的值(目前没有计划删除 DEBUG_BYTECODE_SUFFIXES
和 OPTIMIZED_BYTECODE_SUFFIXES
,但最迟将在 Python 4 中删除)。
所有各种查找器和加载器也将根据需要进行更新,但更新 importlib 中之前提到的部分应该是所有需要做的。
标准库的其他部分
py_compile
和 compileall
函数暴露的各种功能将根据需要进行更新,以确保它们遵循新的字节码文件名称语义 [6],[1]。 compileall
模块的 CLI 不会直接受到影响(-b
标志将是隐式的,因为它不再生成 .pyo
文件,当指定 -O
时)。
兼容性注意事项
从 Python 3.2 开始,任何直接操作字节码文件的代码都需要考虑此更改对其代码的影响(在 Python 3.2 之前——包括所有 Python 2——不存在 __pycache__,这已经需要对字节码文件处理支持进行二分)。如果代码将 debug_override
参数设置为 importlib.util.cache_from_source()
,那么如果他们想要优化级别为 2 的字节码文件的路径,则需要谨慎。否则,只有 **不** 使用 importlib.util.cache_from_source()
的代码才需要更新。
至于那些分发仅包含字节码的模块的人(即,使用字节码文件而不是源文件),他们将不得不选择他们希望字节码文件的优化级别,因为分发一个 .pyo
文件和一个 .pyc
文件将不再有任何用处。由于人们通常只为代码混淆目的或更小的分发大小分发字节码文件,因此只需要分发一个 .pyc
实际上对这些用例是有益的。由于字节码文件的魔数在 Python 3.5 中发生了改变,以支持 PEP 465,因此无需支持现有的 .pyo
文件 [8]。
被拒绝的想法
完全从 CPython 中删除优化级别
有些人建议,与其在 CPython 中适应各种优化级别,不如完全放弃它们。他们的论点是,通过像 JIT 这样的运行时优化,而不是通过预执行字节码优化,可以实现显著的性能提升。
这个想法被这个 PEP 拒绝了,因为它忽略了这样一个事实,即有些人确实发现 CPython 的现有优化级别很有用。它还假设没有其他 Python 解释器会发现这个 PEP 提出的内容有用。
文件名称中优化级别的替代格式
使用“opt-”前缀并将优化级别放在缓存标签和文件扩展名之间不是至关重要的。所有已经考虑过的选项是
importlib.cpython-35.opt-1.pyc
importlib.cpython-35.opt1.pyc
importlib.cpython-35.o1.pyc
importlib.cpython-35.O1.pyc
importlib.cpython-35.1.pyc
importlib.cpython-35-O1.pyc
importlib.O1.cpython-35.pyc
importlib.o1.cpython-35.pyc
importlib.1.cpython-35.pyc
这些最初被拒绝的原因要么是它们会改变字节码文件的排序顺序,要么是与缓存标签可能存在歧义,要么是不够自文档化。进行了一次非正式的投票,人们显然更喜欢 PEP 提出的格式 [9]。由于这个主题是非技术性的,属于个人选择,因此这个问题被认为已经解决。
将优化级别嵌入字节码元数据中
有些人建议,与其将字节码的优化级别嵌入到文件名中,不如将其包含在文件的元数据中。这意味着任何时候每个解释器都只有一个字节码副本。更改优化级别将需要重写字节码,但同时也只需要关心一个文件。
由于 Python 通常安装为根级应用程序,因此始终有可能修改标准库中模块的字节码文件,因此这个建议被拒绝了。在这种情况下,集成者需要猜测在任何/所有情况下对用户来说合理的优化级别是什么。通过允许多个优化级别同时共存,它使集成者不必猜测用户想要什么,并允许用户使用他们想要的优化级别。
参考
版权
此文档已置于公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0488.rst
最后修改时间:2023-09-09 17:39:29 GMT