繁体   English   中英

python中增广运算符(定界符)的求值顺序

[英]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

可以看出,试图将增强赋值的评估顺序总结为“从左到右”只是一个近似值。 如上面的虚拟机代码所示,这是实际发生的情况:

  1. “计算”目标聚合及其索引(第 0 行和第 2 行),然后复制这两个值(第 4 行)。 (重复意味着目标及其下标都不会被计算两次。)

  2. 然后使用重复的值来查找元素的值(第 6 行)。 因此,此时评估a[-1]的值。

  3. 然后计算右侧表达式( a.pop() )(第 8 行到第 12 行)。

  4. 这两个值(在这种情况下都是 3)与INPLACE_ADD组合,因为这是一个ADD增强分配。 在整数的情况下, INPLACE_ADDADD之间没有区别,因为整数是不可变的值。 但是编译器不知道第一个操作数是整数。 a[-1]可以是任何东西,包括另一个列表。 因此,它会发出一个操作数,该操作数将触发使用__iadd__方法而不是__add__ ,以防有区别。

  5. 原始目标和下标,从第 1 步开始就一直在堆栈上耐心等待,然后用于执行下标存储(第 16 和 18 行。下标仍然是在第 2 行-1处计算的下标。但此时a[-1]指的是a的不同元素。需要旋转才能将参数 for 转换为正确的顺序。因为赋值的正常评估顺序是首先评估右侧,虚拟机假设新值将位于堆栈的底部,然后是对象及其下标。

  6. 最后, 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

其中,虽然不可否认是两条线,因此劣势!!! , 实现了相同的结果,但开销没有意义,而且更清晰。

TL;DR:Python 语言规范保证+=的右侧在扩展赋值操作开始之前被完全评估,并且左侧的任何代码都被评估。 但是为了代码清晰,任何依赖于该保证的代码都应该被重构以避免上述依赖。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM