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 不在本文档的范围内:Py_LIMITED_API 宏和稳定的 ABI 以不同的方式解决此问题,请参阅 PEP 384:定义稳定的 ABI

故意破坏向后兼容性的安全修复程序将不会获得兼容性层;安全比兼容性更重要。例如,http.client.HTTPSConnection 在 Python 3.4.3 中进行了修改,以默认执行所有必要的证书和主机名检查。这是一项由 PEP 476:为标准库 http 客户端默认启用证书验证 (bpo-22417) 推动的故意更改。

Python 语言不提供向后兼容性。

本文档不涵盖不明显不兼容的更改。例如,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() 函数意味着应用程序的行为将根据兼容性版本而有所不同。此外,由于版本可以多次降低,因此应用程序的行为可能会根据导入顺序而有所不同。

使用 sys.set_python_compat_version((3, 8)) 的 Python 3.9 与 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 包安装程序,以便不仅为当前 Python 版本预编译 .pyc 文件,还为多个旧版 Python 版本(最多 Python 3.0?)预编译 .pyc 文件。

每个 .py 文件将有 3n 个 .pyc 文件(3 个优化级别),其中 n 是支持的 Python 版本的数量。例如,这意味着 6 个 .pyc 文件,而不是 3 个,以支持 Python 3.8 和 Python 3.9。

不兼容更改的临时暂停

2009 年,PEP 3003“Python 语言暂停”提议对 Python 3.1 和 Python 3.2 的所有 Python 语言语法、语义和内置函数的更改进行临时暂停(暂停)。

2018 年 5 月,在 PEP 572 讨论期间,也有人提议放慢 Python 更改的速度:请参阅 python-dev 线程 Slow down…

Barry Warsaw 对此的呼吁:

我不认为 Python 在未来 10 年保持相关性和实用性的方法是停止所有语言发展。谁知道 5 年后,更不用说 10 年后的计算环境会是什么样子?像 10 年的暂停这样武断的事情(再次,恕我直言)是对这种语言的死刑判决。

PEP 387

PEP 387 – 向后兼容性策略 提出了进行不兼容更改的过程。重点是该过程的第 4 步

查看是否有反馈。在看到警告后,未参与原始讨论的用户现在可以发表评论。也许可以重新考虑。

PEP 497

PEP 497 – 向后兼容的标准机制 提出了提供向后兼容性的不同解决方案。

除了 __past__ 机制的想法外,PEP 497 没有提出具体的解决方案

当对核心语言语法或语义进行不兼容更改时,Python-dev 的策略是优先考虑并期望在默认情况下采用破坏性更改后的未来 Python 版本中考虑并提供向后兼容机制(只要有可能),此外还提供任何为向前兼容性提出的机制,例如新的 future_statements。

不兼容更改的示例

Python 3.8

Python 3.8 不兼容更改的示例

  • (在测试版阶段)PyCode_New() 需要一个新参数:它破坏了所有 Cython 扩展(所有分发预编译 Cython 代码的项目)。此更改已在 3.8 测试版阶段恢复,并添加了一个新的 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
  • PEP 479 已在 Python 3.7 中的所有代码中启用,这意味着在协程和生成器中直接或间接引发的 StopIteration 异常将转换为 RuntimeError 异常。

  • socketserver.ThreadingMixIn.server_close() 现在会等待所有非守护线程完成。将新的 block_on_close 类属性设置为 False 以获得 3.7 之前的行为。
  • struct.Struct.format 类型现在是 str 而不是 bytes
  • datetime.timedeltarepr 已更改,输出中包含关键字参数。
  • tracemalloc.Traceback 帧现在从最旧到最新排序,以与 traceback 保持一致。

为大多数这些更改添加向后兼容性很容易。

另请参阅 Python 3.7 新特性中的 移植到 Python 3.7 部分。

微版本

有时,不兼容的更改会在微版本(major.minor.micro 中的 micro)中引入,以修复错误或安全漏洞。例如包括

  • Python 3.7.2,compileallpy_compile 模块:invalidation_mode 参数的默认值更新为 NoneSOURCE_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_okTrue 且目录存在,如果 mode 与现有目录的模式不匹配,makedirs() 仍会引发错误。由于此行为无法安全地实现,因此在 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。
  • zipfile 模块自 Python 3.6.2 起接受路径类对象。
  • loop.create_future() 已在 Python 3.5.2 中的 asyncio 模块中添加。

对于此类更改,不需要向后兼容代码。

参考文献

已接受的 PEP

PEP 草案


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

上次修改时间:2024-08-20 10:29:32 GMT