[英]Evaluation order of augmented operators (delimiters) in python
如果我在 python 中评估以下最小示例
a = [1, 2, 3]
a[-1] += a.pop()
我明白了
[1, 6]
所以似乎这被评估为
a[-1] = a[-1] + a.pop()
其中每个表达式/操作数将按顺序计算
third = first + second
所以在左侧 a[-1] 是第二个元素,而在右侧是第三个。
a[1] = a[2] + a.pop()
有人可以向我解释如何从文档中推断出这一点吗? 显然 '+=' 在词法上是一个也执行操作的定界符(参见此处)。 这对其评估顺序意味着什么?
编辑:
我试图在评论中澄清我的问题。 我将其包含在此处以供参考。
我想了解在词法分析期间是否必须以特殊方式(即通过扩展它们)处理增强运算符,因为您必须复制一个表达式并对其进行两次评估。 这在文档中并不清楚,我想知道在哪里指定了这种行为。 其他词法定界符(例如'}')表现不同。
让我从你最后问的问题开始:
我想了解在词法分析期间是否必须以特殊方式(即通过扩展它们)处理增强运算符,
那个很简单; 答案是不”。 标记只是一个标记,词法分析器只是将输入划分为标记。 就词法分析器而言, +=
只是一个标记,这就是它为它返回的内容。
顺便说一句,Python 文档区分了“运算符”和“标点符号”,但这对于当前的词法分析器来说并不是真正的显着差异。 在基于运算符优先级解析的解析器的某些先前化身中,这可能是有意义的,其中“运算符”是具有相关优先级和关联性的词位。 但我不知道 Python 是否曾经使用过那个特定的解析算法; 在当前的解析器中,“运算符”和“标点符号”都是在语法规则中出现的字面词位。 如您所料,词法分析器更关心标记的长度( <=
和+=
都是两个字符的标记),而不是解析器中的最终使用。
“脱糖”——将某种语言结构转换为更简单结构的源转换的技术术语——通常不在词法分析器或解析器中执行,尽管编译器的内部工作不受行为准则的约束。 一种语言是否有脱糖组件通常被认为是一个实现细节,并且可能不是特别明显; Python 确实如此。 Python 也不向其标记器公开接口。 tokenizer
模块是纯 Python 中的重新实现,它不会产生完全相同的行为(尽管它足够接近成为有用的探索工具)。 但是解析器暴露在ast
模块中,它提供了对 Python 自己的解析器的直接访问(至少在 CPython 实现中),并且让我们看到在构造 AST 之前没有进行脱糖(注意:需要Python3.9 用于indent
选项):
>>> import ast
>>> def showast(code):
... print(ast.dump(ast.parse(code), indent=2))
...
>>> showast('a[-1] += a.pop()')
Module(
body=[
AugAssign(
target=Subscript(
value=Name(id='a', ctx=Load()),
slice=UnaryOp(
op=USub(),
operand=Constant(value=1)),
ctx=Store()),
op=Add(),
value=Call(
func=Attribute(
value=Name(id='a', ctx=Load()),
attr='pop',
ctx=Load()),
args=[],
keywords=[]))],
type_ignores=[])
这会产生您期望从语法中得到的语法树,其中“增强赋值”语句表示为assignment
中的特定产生式:
assignment:
| single_target augassign ~ (yield_expr | star_expressions)
single_target
是一个单一的可赋值表达式(例如一个变量,或者,在这种情况下,一个下标数组); augassign
是扩充赋值运算符之一,其余的是赋值右侧的替代操作。 (您可以忽略“栅栏”语法运算符~
。) ast.dump
生成的解析树非常接近语法,并且根本没有脱糖:
--------------------------
| | |
Subscript Add Call
--------- -----------------
| | | | |
a -1 Attribute [ ] [ ]
---------
| |
a 'pop'
奇迹发生在之后,我们也可以看到,因为 Python 标准库还包含一个反汇编程序:
>>> import dis
>>> dis.dis(compile('a[-1] += a.pop()', '--', 'exec'))
1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (-1)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_NAME 0 (a)
10 LOAD_METHOD 1 (pop)
12 CALL_METHOD 0
14 INPLACE_ADD
16 ROT_THREE
18 STORE_SUBSCR
20 LOAD_CONST 1 (None)
22 RETURN_VALUE
可以看出,试图将增强赋值的评估顺序总结为“从左到右”只是一个近似值。 如上面的虚拟机代码所示,这是实际发生的情况:
“计算”目标聚合及其索引(第 0 行和第 2 行),然后复制这两个值(第 4 行)。 (重复意味着目标及其下标都不会被计算两次。)
然后使用重复的值来查找元素的值(第 6 行)。 因此,此时评估a[-1]
的值。
然后计算右侧表达式( a.pop()
)(第 8 行到第 12 行)。
这两个值(在这种情况下都是 3)与INPLACE_ADD
组合,因为这是一个ADD
增强分配。 在整数的情况下, INPLACE_ADD
和ADD
之间没有区别,因为整数是不可变的值。 但是编译器不知道第一个操作数是整数。 a[-1]
可以是任何东西,包括另一个列表。 因此,它会发出一个操作数,该操作数将触发使用__iadd__
方法而不是__add__
,以防有区别。
原始目标和下标,从第 1 步开始就一直在堆栈上耐心等待,然后用于执行下标存储(第 16 和 18 行。下标仍然是在第 2 行-1
处计算的下标。但此时a[-1]
指的是a
的不同元素。需要旋转才能将参数 for 转换为正确的顺序。因为赋值的正常评估顺序是首先评估右侧,虚拟机假设新值将位于堆栈的底部,然后是对象及其下标。
最后, None
作为语句的值返回。
Python 参考手册中记录了赋值语句和增强赋值语句的精确工作原理。 另一个重要的信息来源是对__iadd__
特殊方法的描述。 增强赋值操作的评估(和评估顺序)非常令人困惑,以至于有一个专门针对它的编程常见问题解答,如果您想了解确切的机制,值得仔细阅读。
尽管这些信息很有趣,但值得补充的是,编写依赖于增强赋值中评估顺序细节的程序不利于生成可读代码。 在几乎所有情况下,都应避免依赖于过程的非显而易见细节的增强分配,包括诸如作为该问题目标的陈述。
rici 很好地展示了 CPython 参考解释器的幕后情况,但语言规范中有一个更简单的“事实来源”,它保证了任何Python 解释器(不仅仅是 CPython,还有 PyPy、Jython 、IronPython、Cython 等)。 在语言规范中,在第 6 章:表达式,第 6.16 节,评估顺序下,它指定:
Python 从左到右计算表达式。 请注意,在评估分配时,右侧先于左侧评估。
第二句话听起来像是一般规则的例外,但事实并非如此。 带=
的赋值(包括带+=
之类的扩展赋值)不是 Python 中的表达式( 3.8 中引入的 walrus 运算符是一个表达式,但它只能分配给裸名,因此永远没有任何东西可以“评估”左侧,它纯粹存储在那里,从不读取),它是一个语句,并且赋值语句有自己的评估顺序规则。 这些分配规则指定:
赋值语句计算表达式列表(请记住,这可以是单个表达式或逗号分隔的列表,后者产生一个元组)并将单个结果对象从左到右分配给每个目标列表。
这证实了表达评估令文件中的第二句话; 首先评估表达式列表(要分配的事物),然后从那里开始对目标的分配。 因此,根据语言规范本身, a[-1] += a.pop()
必须首先完全评估a.pop()
(“表达式列表”),然后执行赋值。
这种行为是语言规范所要求的,并且已经有一段时间了,因此无论您使用什么 Python 解释器都可以依赖它。
也就是说,我建议不要使用依赖于 Python 的这些保证的代码。 一方面,当您切换到其他语言时,规则会有所不同(在某些情况下,例如 C 和 C++ 中的许多类似情况,因标准版本而异,没有“规则”,并试图在表达式的多个部分会产生未定义的行为),因此越来越依赖 Python 的行为会妨碍您使用其他语言的能力。 除此之外,它仍然会令人困惑,只需稍作更改即可避免混淆,例如,在您的情况下,更改:
a[-1] += a.pop()
只是:
x = a.pop()
a[-1] += x
其中,虽然不可否认是两条线,因此劣势!!! , 实现了相同的结果,但开销没有意义,而且更清晰。
+=
的右侧在扩展赋值操作开始之前被完全评估,并且左侧的任何代码都被评估。 但是为了代码清晰,任何依赖于该保证的代码都应该被重构以避免上述依赖。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.