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 不在本文档的范围内: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 中新的 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()
函数意味着应用程序的行为将根据兼容性版本而有所不同。此外,由于版本可以多次降低,因此应用程序的行为可能会根据导入顺序而有所不同。
使用 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
参数,用于不将 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 包安装程序,以便不仅为当前 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…
我不认为 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 不兼容更改的示例
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
。- PEP 479 已在 Python 3.7 中的所有代码中启用,这意味着在协程和生成器中直接或间接引发的
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
且目录存在,如果 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 草案
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0606.rst
上次修改时间:2024-08-20 10:29:32 GMT