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

Python 增强提案

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 命令行选项。添加 PYTHONCOMPATVERSIONPYTHONCOMPATMINVERSION 环境变量。

基本原理

频繁演进的需求

为了保持相关性和有用性,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 中新的 DeprecationWarningPendingDeprecatingWarning 警告在 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 起已被弃用并发出 DeprecationWarningbpo-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.micromajor.minor)的版本字符串。例如,-X compat_version=3.8 调用 sys.set_python_compat_version((3, 8))

添加 PYTHONCOMPATVERSIONVERSION=VERSIONPYTHONCOMPATMINVERSION=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 参数,用于不将 asyncawait 视为关键字。

最新的主要语言向后不兼容更改是 Python 3.7,它将 asyncawait 变为真正的关键字。似乎只有 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 模块的代码需要 asyncawait 是真正的关键字(此更改在 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…

Barry Warsaw 的评论:

我认为 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 不兼容更改的示例

  • asyncawait 现在是保留关键字。
  • 移除了几个未文档化的内部导入。一个例子是 os.errno 不再可用;请直接使用 import errno。请注意,此类未文档化的内部导入随时可能在没有任何通知的情况下被移除,即使是在微版本发布中。
  • re.sub() 的替换模板中,由 '\' 和 ASCII 字母组成的未知转义在 Python 3.5 中已被弃用,现在将导致错误。
  • 已移除 asyncio.windows_utils.socketpair() 函数:它是 socket.socketpair() 的别名。
  • asyncio 不再将 selectors_overlapped 模块导出为 asyncio.selectorsasyncio._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,compileallpy_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.clientHTTPSConnection 默认执行所有必要的证书和主机名检查。
  • Python 3.4.2,email.messageEmailMessage.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


来源:https://github.com/python/peps/blob/main/peps/pep-0606.rst

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