PEP 236 – 回溯到 __future__
- 作者:
- Tim Peters <tim.peters at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2001年2月26日
- Python 版本:
- 2.1
- 发布历史:
- 2001年2月26日
动机
Python 不时地对核心语言构造的声明语义进行不兼容的更改,或以某种方式更改其偶然(取决于实现)行为。虽然这绝不是武断的,并且始终以长期改进语言为目标,但在短期内,它是存在争议且具有破坏性的。
PEP 5,《语言演进指南》提出了缓解痛苦的方法,本PEP引入了一些支持该方法的机制。
PEP 227,《静态嵌套作用域》是第一个应用,将在此处用作示例。
意图
[注意:这是策略,因此最终应移至 PEP 5]
当对核心语言语法或语义进行不兼容的更改时
- 引入此更改的发布版本 C 默认不更改语法或语义。
- 指定一个未来的发布版本 R,在该版本中将强制执行新的语法或语义。
- PEP 230,《警告框架》中描述的机制用于在可能的情况下生成警告,警告有关其含义可能[1]在发布版本 R 中更改的构造或操作。
- 新的 future_statement(见下文)可以显式包含在模块 M 中,以请求模块 M 中的代码在当前发布版本 C 中使用新的语法或语义。
因此,旧代码默认情况下仍可工作,至少一个发布版本,尽管它可能会开始生成新的警告消息。在此期间,可以使用 future_statement 来迁移到新的语法或语义,使包含它的模块表现得像新的语法或语义已经被强制执行一样。
请注意,除非新功能可能破坏现有代码,否则无需将 future_statement 机制引入新功能;完全向后兼容的添加可以——也应该——在没有相应 future_statement 的情况下引入。
语法
future_statement 只是一个使用保留模块名 __future__
的 from/import 语句
future_statement: "from" "__future__" "import" feature ["as" name]
(","feature ["as" name])*
feature: identifier
name: identifier
此外,所有 future_statement 必须出现在模块的顶部附近。唯一可以出现在 future_statement 之前的行是
- 模块文档字符串(如果有)。
- 注释。
- 空行。
- 其他 future_statement。
示例
"""This is a module docstring."""
# This is a comment, preceded by a blank line and followed by
# a future_statement.
from __future__ import nested_scopes
from math import sin
from __future__ import alabaster_weenoblobs # compile-time error!
# That was an error because preceded by a non-future_statement.
语义
future_statement 在编译时被识别并特殊处理:核心构造语义的更改通常通过生成不同的代码来实现。甚至可能出现新功能引入新的不兼容语法(例如新的保留字)的情况,在这种情况下,编译器可能需要以不同的方式解析模块。此类决策不能推迟到运行时。
对于任何给定的发布版本,编译器都知道哪些功能名称已定义,如果 future_statement 包含它不知道的功能,则会引发编译时错误[2]。
直接的运行时语义与任何 import
语句相同:有一个标准模块 __future__.py
,后面会描述,它将在执行 future_statement 时以通常的方式导入。
有趣的 运行时语义取决于模块中出现的 future_statement “导入”的特定功能。
请注意,该语句并没有什么特别之处
import __future__ [as name]
这不是一个 future_statement;它是一个普通的 import 语句,没有特殊的语义或语法限制。
示例
考虑文件 scope.py 中的这段代码
x = 42
def f():
x = 666
def g():
print "x is", x
g()
f()
在2.0版本下,它打印
x is 42
嵌套作用域(PEP 227)在2.1版本中引入。但在2.1版本下,它仍然打印
x is 42
并生成一个警告。
在2.2版本中,以及在2.1版本中,如果 from __future__ import nested_scopes
包含在 scope.py
的顶部,它打印
x is 666
标准模块 __future__.py
Lib/__future__.py
是一个真正的模块,它有三个目的
- 为了避免混淆分析 import 语句并期望找到它们正在导入的模块的现有工具。
- 为了确保在2.1之前的版本下运行的 future_statements 至少产生运行时异常(
__future__
的导入将失败,因为在2.1之前没有该名称的模块)。 - 为了记录不兼容更改何时引入,以及何时它们将——或已经——强制执行。这是一种可执行文档的形式,可以通过导入
__future__
并检查其内容来以编程方式进行检查。
__future__.py
中的每个语句都采用以下形式
FeatureName = "_Feature(" OptionalRelease "," MandatoryRelease ")"
其中,通常情况下,OptionalRelease < MandatoryRelease,两者都是与 sys.version_info
相同形式的5元组
(PY_MAJOR_VERSION, # the 2 in 2.1.0a3; an int
PY_MINOR_VERSION, # the 1; an int
PY_MICRO_VERSION, # the 0; an int
PY_RELEASE_LEVEL, # "alpha", "beta", "candidate" or "final"; string
PY_RELEASE_SERIAL # the 3; an int )
OptionalRelease 记录了第一个接受以下内容的发布版本
from __future__ import FeatureName
被接受了。
对于尚未发生的 MandatoryRelease,MandatoryRelease 预测了该功能将成为语言一部分的发布版本。
否则,MandatoryRelease 记录了该功能何时成为语言的一部分;在该版本或之后的版本中,模块不再需要
from __future__ import FeatureName
使用相关功能,但可以继续使用此类导入。
MandatoryRelease 也可以是 None
,这意味着计划的功能被放弃了。
_Feature
类的实例有两个对应的方法,.getOptionalRelease()
和 .getMandatoryRelease()
。
从 __future__.py
中永远不会删除任何功能行。
示例行
nested_scopes = _Feature((2, 1, 0, "beta", 1), (2, 2, 0, "final", 0))
这意味着
from __future__ import nested_scopes
将在2.1b1或更高版本中生效,并且嵌套作用域预计将从2.2版本开始强制执行。
已解决的问题:运行时编译
几个Python功能可以在模块运行时编译代码
exec
语句。execfile()
函数。compile()
函数。eval()
函数。input()
函数。
由于包含命名特性 F 的 future_statement 的模块 M 明确请求当前版本在 F 方面表现得像未来版本,因此从 M 内部传递给其中一个函数的文本动态编译的任何代码也可能应该使用与 F 相关联的新语法或语义。2.1 版本确实是这样表现的。
然而,这并非总是期望的。例如,doctest.testmod(M)
从 M 中的字符串编译示例,这些示例应该使用 M 的选择,而不是必需使用 doctest 模块的选择。在2.1版本中,这是不可能的,并且还没有提出任何方案来解决这个问题。注意:PEP 264 后来通过向 compile()
添加可选参数以灵活的方式解决了这个问题。
无论如何,出现在由 exec
、execfile()
或 compile()
动态编译的文本“顶部附近”(参见上面的语法)的 future_statement 适用于生成的代码块,但对执行此类 exec
、execfile()
或 compile()
的模块没有进一步影响。然而,这不能用于影响 eval()
或 input()
,因为它们只允许表达式输入,而 future_statement 不是表达式。
已解决的问题:原生交互式Shell
有两种获取交互式 Shell 的方法
- 通过不带脚本参数从命令行调用 Python。
- 通过带
-i
开关和脚本参数从命令行调用 Python。
交互式 Shell 可以看作是运行时编译的一种极端情况(见上文):实际上,在交互式 Shell 提示符下输入的每个语句都会运行 exec
、compile()
或 execfile()
的新实例。在交互式 Shell 中输入的 future_statement 适用于 Shell 会话的其余生命周期,就像 future_statement 出现在模块顶部一样。
已解决的问题:模拟交互式Shell
“手工构建”的交互式 Shell(由IDLE和Emacs Python模式等工具构建)应该表现得像原生交互式Shell(见上文)。然而,原生交互式Shell内部使用的机制尚未公开,并且对于构建自己的交互式Shell的工具来说,没有明确的方法来实现所需的行为。
注意:PEP 264 后来解决了这个问题,通过向标准 codeop.py
添加智能。不使用标准库 shell 助手工具的模拟 shell 可以通过利用 PEP 264 添加的 compile()
的新可选参数来获得类似的效果。
问答
那“from __past__”版本呢,用来恢复 旧的 行为?
超出本PEP的范围。不过,作者认为可能性不大。如果您想继续,请撰写一个PEP。
那由于Python虚拟机更改而导致的不兼容性呢?
超出本PEP的范围,尽管 PEP 5 也建议了宽限期,并且 future_statement 可能在那方面也发挥作用。
那由于Python的C API更改而导致的不兼容性呢?
超出本PEP的范围。
我想把 future_statements 放在 try/except 块中,这样我就可以根据我正在运行的Python版本使用不同的代码。为什么不行?
抱歉!try/except
是运行时特性;future_statements 主要是在编译时生效的机制,而你的 try/except
在编译器完成很久之后才发生。也就是说,当你执行 try/except
时,对模块生效的语义早已尘埃落定。由于 try/except
无法实现它 看起来 应该实现的功能,因此它根本不允许。我们还希望这些特殊语句非常容易找到和识别。
请注意,您 可以 直接导入 __future__
,并利用其中的信息,结合 sys.version_info
,来确定您正在运行的版本与给定功能的状况之间的关系。
回到嵌套作用域的例子,如果发布了2.2版本而我还没有更改我的代码怎么办?那时我怎么才能保持2.1的行为?
通过继续使用2.1版本,直到您更改代码才升级到2.2版本。future_statement 的目的是让那些及时更新到最新版本的人生活更轻松。如果您不这样做,我们不会讨厌您,但您的问题更难解决,并且有这些问题的人需要撰写一个PEP来解决它们。future_statement 针对的是不同的受众。
重载 import
很糟糕。为什么不为此引入一个新语句?
比如 lambda lambda nested_scopes
?也就是说,除非我们引入一个新的关键字,否则我们无法引入一个全新的语句。但是,如果我们引入一个新的关键字,那本身就会破坏旧代码。那会太讽刺了,无法忍受。是的,重载 import
确实很糟糕,但不如替代方案那么糟糕——就目前而言,future_statements 是100%向后兼容的。
版权
本文档已置于公共领域。
参考文献和脚注
来源:https://github.com/python/peps/blob/main/peps/pep-0236.rst