PEP 606 – Python 兼容性版本
- 作者:
- Victor Stinner <vstinner at python.org>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建日期:
- 2019 年 10 月 18 日
- Python 版本:
- 3.9
摘要
添加 sys.set_python_compat_version(version) 以便与请求的 Python 版本实现部分兼容性。添加 sys.get_python_compat_version()。
修改标准库中的一些函数以实现与 Python 3.8 的部分兼容性。
添加 sys.set_python_min_compat_version(version) 以拒绝与 version 早于该版本的 Python 版本的向后兼容性。
添加 -X compat_version=VERSION 和 -X min_compat_version=VERSION 命令行选项。添加 PYTHONCOMPATVERSION 和 PYTHONCOMPATMINVERSION 环境变量。
基本原理
频繁演进的需求
为了保持相关性和有用性,Python 必须频繁演进;一些增强功能需要不兼容的更改。任何不兼容的更改都可能破坏未知数量的 Python 项目。开发者可能会因此决定不实现某个功能。
用户希望获得最新的 Python 版本以获得新功能和更好的性能。一些不兼容的更改可能会阻止他们在使用最新的 Python 版本上运行他们的应用程序。
本 PEP 提议添加对旧 Python 版本的部分兼容性,作为权衡以适应这两种用例。
从 Python 2 迁移到 Python 3 的主要问题不在于 Python 3 不向后兼容,而在于不兼容更改的引入方式。
部分兼容性以最大程度地减少 Python 的维护负担
虽然从技术上讲可以为旧 Python 版本提供完整的兼容性,但本 PEP 提议最小化处理向后兼容性的函数数量,以减少 Python 项目(CPython)的维护负担。
每项引入对函数进行向后兼容性支持的更改都应经过充分讨论,以评估长期维护成本。
向后兼容性代码将在每个 Python 版本发布时被删除,按具体情况处理。每个兼容性函数可以支持不同数量的 Python 版本发布,具体取决于其维护成本和删除该功能时预期的风险(受影响的项目数量)。
维护成本不仅来自实现向后兼容性的代码,还来自额外的测试。
不兼容的向后兼容性的情况
当调用 sys.set_python_compat_version() 时,任何兼容性代码的性能开销必须很低。
C API 不在此 PEP 的范围内:Py_LIMITED_API 宏和稳定的 ABI 通过不同的方式解决了这个问题,请参阅 PEP 384:定义稳定的 ABI。
故意破坏向后兼容性的安全修复程序将不会有兼容性层;安全比兼容性更重要。例如,http.client.HTTPSConnection 在 Python 3.4.3 中进行了修改,默认执行所有必要的证书和主机名检查。这是由 PEP 476:为标准库 http 客户端启用证书验证(默认)(bpo-22417)驱动的故意更改。
Python 语言不提供向后兼容性。
不明确不兼容的更改不在本 PEP 的讨论范围内。例如,Python 3.9 将 pickle 模块的默认协议更改为协议 4,该协议最早在 Python 3.4 中引入。此更改与 Python 3.4 向后兼容。当请求与 Python 3.8 兼容时,无需默认使用协议 3。
Python 3.9 中新的 DeprecationWarning 和 PendingDeprecatingWarning 警告在 Python 3.8 兼容模式下不会被禁用。如果项目使用 -Werror(将任何警告视为错误)运行其测试套件,则必须修复这些警告,或逐例忽略特定的弃用警告。
将项目升级到更新的 Python 版本
没有向后兼容性,所有不兼容的更改都必须一次性修复,这可能是一个阻塞问题。当一个项目升级到比旧 Python 版本晚多个版本的更新 Python 版本时,情况会更糟。
推迟升级只会使情况变得更糟:跳过的每个版本都会增加更多不兼容的更改。技术债务只会随着时间的推移而稳步增加。
有了向后兼容性,就可以在项目中逐步升级 Python,而无需一次性修复所有问题。
“全有或全无”是迁移大型 Python 2 代码库到 Python 3 的一个绊脚石。Python 2 和 Python 3 之间不兼容更改的列表很长,并且随着每个 Python 3.x 版本的发布而增长。
清理 Python 和 DeprecationWarning
《Python 之禅》(PEP 20)的座右铭之一是:
应该有一个–而且最好只有一个–显而易见的方法来做到这一点。
当 Python 演进时,新的方法不可避免地会出现。DeprecationWarning 被发出以建议使用新的方法,但许多开发者会忽略这些警告,因为它们默认是静默的(除了在 __main__ 模块中:请参阅 PEP 565)。一些开发者在警告过多时干脆忽略所有警告,因此只在已弃用代码被删除时才会关心异常。
有时,同时支持两种方式的维护成本很小,但开发者宁愿放弃旧的方式来清理他们的代码。这些更改属于不兼容的向后更改。
一些开发者可以将 Python 2 支持的结束作为机会,推出比平时更多的非兼容性更改。
添加选择性的向后兼容性可以防止应用程序中断,并允许开发人员继续进行这些清理工作。
重新分配维护负担
向后兼容性使得不兼容更改的作者更多地参与到升级路径中。
向后兼容性的示例
collections ABC 别名
collections.abc 对 ABC 类的别名已从 Python 3.9 的 collections 模块中移除,在此之前已从 Python 3.3 开始弃用。例如,collections.Mapping 不再存在。
在 Python 3.6 中,通过 from _collections_abc import * 在 collections/__init__.py 中创建了别名。
在 Python 3.7 中,在 collections 模块中添加了一个 __getattr__(),以便在第一次访问属性时发出 DeprecationWarning。
def __getattr__(name):
# For backwards compatibility, continue to make the collections ABCs
# through Python 3.6 available through the collections module.
# Note: no new collections ABCs were added in Python 3.7
if name in _collections_abc.__all__:
obj = getattr(_collections_abc, name)
import warnings
warnings.warn("Using or importing the ABCs from 'collections' instead "
"of from 'collections.abc' is deprecated since Python 3.3, "
"and in 3.9 it will be removed.",
DeprecationWarning, stacklevel=2)
globals()[name] = obj
return obj
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
通过在向后兼容性被请求时,添加回 __getattr__() 函数,可以在 Python 3.9 中恢复与 Python 3.8 的兼容性,但这仅在请求向后兼容性时才有效。
def __getattr__(name):
if (sys.get_python_compat_version() < (3, 9)
and name in _collections_abc.__all__):
...
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
已弃用的 open() “U” 模式
open() 的 "U" 模式自 Python 3.4 起已被弃用并发出 DeprecationWarning。bpo-37330 提议删除此模式:open(filename, "rU") 将引发异常。
此更改属于“清理”类别:它不是实现某个功能所必需的。
向后兼容模式易于实现,并且会受到用户的欢迎。
规范
sys 函数
向 sys 模块添加 3 个函数
sys.set_python_compat_version(version):设置 Python 兼容性版本。如果之前已调用过,则使用请求版本的最小值。如果调用了sys.set_python_min_compat_version(min_version)且version < min_version,则引发异常。version 必须大于或等于(3, 0)。sys.set_python_min_compat_version(min_version):设置**最低**兼容性版本。如果之前调用了sys.set_python_compat_version(old_version)且old_version < min_version,则引发异常。min_version 必须大于或等于(3, 0)。sys.get_python_compat_version():获取 Python 兼容性版本。返回一个包含 3 个整数的tuple。
version 必须是包含 2 或 3 个整数的元组。(major, minor) 版本等同于 (major, minor, 0)。
默认情况下,sys.get_python_compat_version() 返回当前的 Python 版本。
例如,请求与 Python 3.8.0 兼容
import collections
sys.set_python_compat_version((3, 8))
# collections.Mapping alias, removed from Python 3.9, is available
# again, even if collections has been imported before calling
# set_python_compat_version().
parent = collections.Mapping
显然,调用 sys.set_python_compat_version(version) 对调用之前的代码没有影响。使用 -X compat_version=VERSION 命令行选项或 PYTHONCOMPATVERSIONVERSION=VERSION 环境变量可以在 Python 启动时设置兼容性版本。
命令行
添加 -X compat_version=VERSION 和 -X min_compat_version=VERSION 命令行选项:分别调用 sys.set_python_compat_version() 和 sys.set_python_min_compat_version()。 VERSION 是一个包含 2 或 3 个数字(major.minor.micro 或 major.minor)的版本字符串。例如,-X compat_version=3.8 调用 sys.set_python_compat_version((3, 8))。
添加 PYTHONCOMPATVERSIONVERSION=VERSION 和 PYTHONCOMPATMINVERSION=VERSION=VERSION 环境变量:分别调用 sys.set_python_compat_version() 和 sys.set_python_min_compat_version()。 VERSION 是一个格式与命令行选项相同的版本字符串。
向后兼容性
引入 sys.set_python_compat_version() 函数意味着应用程序的行为会因兼容性版本而异。此外,由于版本可以多次降低,应用程序的行为可能会因导入顺序而异。
Python 3.9 配合 sys.set_python_compat_version((3, 8)) 并不完全兼容 Python 3.8:兼容性只是部分性的。
安全隐患
sys.set_python_compat_version() 不得禁用安全修复。
备选方案
为每次不兼容的更改提供变通方法
应用程序可以处理影响到它的绝大多数不兼容更改。
例如,可以使用以下方法恢复 collections 别名:
import collections.abc
collections.Mapping = collections.abc.Mapping
collections.Sequence = collections.abc.Sequence
在解析器中处理向后兼容性
修改了解析器以支持多个 Python 语言版本(语法)。
当前的 Python 解析器不易于为此进行修改。AST 和语法被硬编码为单一 Python 版本。
在 Python 3.8 中,compile() 有一个未文档化的 _feature_version 参数,用于不将 async 和 await 视为关键字。
最新的主要语言向后不兼容更改是 Python 3.7,它将 async 和 await 变为真正的关键字。似乎只有 Twisted 受到了影响,并且 Twisted 有一个受影响的函数(它使用了一个名为 async 的参数)。
在解析器中处理向后兼容性似乎相当复杂,不仅要修改解析器,还要让开发者检查正在使用的 Python 语言版本。
from __future__ import python38_syntax
向 __future__ 模块添加 pythonXY_syntax。它将启用与 Python X.Y 语法的向后兼容性,但仅限于当前文件。
有了此选项,就不需要更改 sys.implementation.cache_tag 来使用不同的 .pyc 文件名,因为解析器将始终为相同的输入生成相同的输出(除了优化级别)。
例如
from __future__ import python35_syntax
async = 1
await = 2
更新 cache_tag
修改解析器以使用 sys.get_python_compat_version() 来选择 Python 语言版本。
sys.set_python_compat_version() 更新 sys.implementation.cache_tag 以包含不带微版本的兼容性版本作为后缀。例如,Python 3.9 默认使用 'cpython-39',但是 sys.set_python_compat_version((3, 7, 2)) 将 cache_tag 设置为 'cpython-39-37'。现在允许在微版本发布中更改 Python 语言。
一个问题是,如果之前调用了 sys.set_python_compat_version((3, 6)),那么 import asyncio 很可能会失败。asyncio 模块的代码需要 async 和 await 是真正的关键字(此更改在 Python 3.7 中完成)。
另一个问题是,普通用户无法将 .pyc 文件写入系统目录,因此无法按需创建它们。这意味着在向后兼容模式下无法使用 .pyc 优化。
解决此问题的一种方法是修改 Python 安装程序和 Python 包安装程序,以预编译 .pyc 文件,不仅为当前 Python 版本,还为多个旧 Python 版本(最多到 Python 3.0?)。
每个 .py 文件将有 3n 个 .pyc 文件(3 个优化级别),其中 n 是支持的 Python 版本数。例如,这意味着支持 Python 3.8 和 Python 3.9 需要 6 个 .pyc 文件,而不是 3 个。
暂时禁止不兼容的更改
2009 年,PEP 3003 “Python 语言暂停”提议对 Python 3.1 和 Python 3.2 的 Python 语言语法、语义和内置函数进行临时暂停( moratorium)。
2018 年 5 月,在 PEP 572 的讨论期间,还提议放慢 Python 的变化:请参阅 python-dev 邮件列表 Slow down…
我认为 Python 在未来 10 年内保持相关性和可用性的方式是停止所有语言演进。谁知道 5 年后,甚至 10 年后,计算格局会是什么样子?像 10 年暂停这样任意的事情(再次, IMHO)对这门语言来说是判了死刑。
PEP 387
PEP 387 – 向后兼容策略 提出了一种进行不兼容更改的流程。要点是流程的第 4 步:
查看是否有任何反馈。未参与最初讨论的用户现在可以看到警告后发表评论。也许会重新考虑。
PEP 497
PEP 497 – 标准的向后兼容机制 提出提供向后兼容性的不同解决方案。
除了 __past__ 机制的想法之外,PEP 497 没有提出具体的解决方案。
当对核心语言语法或语义进行不兼容更改时,Python-dev 的政策是优先并期望在可能的情况下,在默认采用破坏性更改后,为未来的 Python 版本考虑并提供向后兼容性机制,此外还提供向前兼容性机制,例如新的 future_statements。
不兼容更改的示例
Python 3.8
Python 3.8 不兼容更改的示例
- (在 beta 阶段)
PyCode_New()需要一个新参数:这破坏了所有 Cython 扩展(所有分发预编译 Cython 代码的项目)。此更改在 3.8 beta 阶段被撤销,并添加了一个新的PyCode_NewWithPosOnlyArgs()函数。 types.CodeType需要一个额外的强制参数。添加了CodeType.replace()函数以帮助项目不再依赖CodeType构造函数的精确签名。- C 扩展不再链接到 libpython。
sys.abiflags从'm'更改为空字符串。例如,python3.8m程序已消失。- C 结构
PyInterpreterState被设为不透明。 - XML 属性顺序:bpo-34160。受影响的项目
无法为所有这些更改添加向后兼容性。例如,C API 和构建系统中的更改不在本 PEP 的范围内。
有关所有更改,请参阅 Python 3.8 中的新增内容:API 和功能移除。
另请参阅 Python 3.8 移植到 Python 3.8 部分。
Python 3.7
Python 3.7 不兼容更改的示例
async和await现在是保留关键字。- 移除了几个未文档化的内部导入。一个例子是
os.errno不再可用;请直接使用import errno。请注意,此类未文档化的内部导入随时可能在没有任何通知的情况下被移除,即使是在微版本发布中。 - 在
re.sub()的替换模板中,由'\'和 ASCII 字母组成的未知转义在 Python 3.5 中已被弃用,现在将导致错误。 - 已移除
asyncio.windows_utils.socketpair()函数:它是socket.socketpair()的别名。 asyncio不再将selectors和_overlapped模块导出为asyncio.selectors和asyncio._overlapped。请将from asyncio import selectors替换为import selectors。- Python 3.7 中对所有代码启用了 PEP 479,这意味着在协程和生成器中直接或间接引发的
StopIteration异常会被转换为RuntimeError异常。 socketserver.ThreadingMixIn.server_close()现在会等待所有非守护线程完成。将新的block_on_close类属性设置为False以获得 3.7 之前的行为。struct.Struct.format的类型现在是str而不是bytes。datetime.timedelta的 `repr` 已更改,在输出中包含关键字参数。tracemalloc.Traceback帧现在从最旧到最新排序,以与traceback保持一致。
为这些更改中的大部分添加向后兼容性将是容易的。
另请参阅 Python 3.7 移植到 Python 3.7 部分。
微版本发布
有时,不兼容的更改会在微版本发布(major.minor.micro 中的 micro)中引入,以修复错误或安全漏洞。示例包括:
- Python 3.7.2,
compileall和py_compile模块:*invalidation_mode* 参数的默认值更新为None;*SOURCE_DATE_EPOCH* 环境变量不再覆盖 *invalidation_mode* 参数的值,而是决定其默认值。 - Python 3.7.1,
xml模块:SAX 解析器默认不再处理通用外部实体,以提高默认安全性。 - Python 3.5.2,
os.urandom():在 Linux 上,如果 getrandom() 系统调用阻塞(urandom 熵池尚未初始化),则回退到读取/dev/urandom。 - Python 3.5.1,
sys.setrecursionlimit():如果新的限制在当前递归深度下太低,现在会引发RecursionError异常。 - Python 3.4.4,
ssl.create_default_context():RC4 已从默认密码字符串中删除。 - Python 3.4.3,
http.client:HTTPSConnection默认执行所有必要的证书和主机名检查。 - Python 3.4.2,
email.message:EmailMessage.is_attachment()现在是方法而不是属性,与Message.is_multipart()保持一致。 - Python 3.4.1,
os.makedirs(name, mode=0o777, exist_ok=False):在 Python 3.4.1 之前,如果 *exist_ok* 为True且目录存在,makedirs()仍然会在 *mode* 与现有目录的模式不匹配时引发错误。由于这种行为无法安全实现,因此在 Python 3.4.1 中将其移除(bpo-21082)。
在微版本发布中引入的非向后不兼容更改的示例
ssl.OP_NO_TLSv1_3常量已添加到 2.7.15、3.6.3 和 3.7.0 中,以与 OpenSSL 1.0.2 向后兼容。typing.AsyncContextManager已添加到 Python 3.6.2 中。- 自 Python 3.6.2 起,
zipfile模块接受类路径对象。 asyncio模块中的loop.create_future()已添加到 Python 3.5.2 中。
这些类型的更改不需要向后兼容代码。
参考资料
已接受的 PEP
草案 PEP
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0606.rst