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()

其中每个 expression/operand 将按顺序计算

third = first + second

所以左边 a[-1] 是第二个元素,而右边是第三个元素。

a[1] = a[2] + a.pop()

谁能给我解释一下如何从 docs? Apparently '+=' is lexically a delimiter that also performs an operation (see here) 中推断出这一点。这对其评估顺序意味着什么?

编辑:

我试图在评论中澄清我的问题。我会把它放在这里供参考。

I want to understand if augmented operators have to be treated in a special way (i.e. by expanding them) during lexical analysis, because you kind of have to duplicate an expression and evaluate it twice. This is not clear in the docs and I want to know where this behaviour is specified. Other lexical delimiters (e.g. '}') behave differently.

让我从你最后问的问题开始:

I want to understand if augmented operators have to be treated in a special way (i.e. by expanding them) during lexical analysis,

那个很简单;答案是不”。令牌只是一个令牌,词法分析器只是将输入分成多个令牌。对于词法分析器而言,+=只是一个token,对它来说returns就是这样。

顺便说一句,Python 文档对“运算符”和“标点符号”进行了区分,但对于当前的词法分析器而言,这并不是真正的显着差异。在基于 operator-precedence 解析的解析器的某些先前化身中可能有意义,其中“运算符”是具有关联优先级和关联性的词素。但我不知道 Python 是否曾经使用过该特定的解析算法;在当前的解析器中,“operators”和“punctuation”都是在句法规则中出现的字面词位。如您所料,词法分析器更关心标记的长度(<=+= 都是 two-character 标记)而不是解析器内部的最终使用。

“脱糖”——源代码转换的技术术语,将一些语言结构转换为更简单的结构——通常不在词法分析器或解析器中执行,尽管编译器的内部工作不受制于行为准则。一种语言是否有脱糖组件通常被认为是一个实现细节,可能不是特别明显; Python 确实如此。 Python 也不向其标记器公开接口; tokenizer 模块是纯 Python 中的重新实现,它不会产生完全相同的行为(尽管它足够接近成为一个有用的探索工具)。但是解析器暴露在 ast 模块中,它提供了对 Python 自己的解析器的直接访问(至少在 CPython 实现中),让我们看到没有 desugaring在构建 AST 之前完成(注意:indent 选项需要 Python3.9):

>>> 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 是增强的赋值运算符之一,其余的是赋值的 right-hand 端的替代方案。 (你可以忽略“fence”语法运算符 ~。)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

可以看出,试图将增广赋值的求值顺序概括为“left-to-right”只是一种近似。下面是实际发生的情况,如上面的虚拟机代码所示:

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

  2. 然后重复的值用于查找元素的值(第 6 行)。所以在这一点上,a[-1] 的值被评估。

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

  4. 这两个值(在本例中均为 3)与 INPLACE_ADD 组合,因为这是一个 ADD 扩充赋值。对于整数,INPLACE_ADDADD 之间没有区别,因为整数是不可变的值。但是编译器不知道第一个操作数是一个整数。 a[-1] 可以是任何东西,包括另一个列表。所以它发出一个操作数,它将触发使用 __iadd__ 方法而不是 __add__,以防存在差异。

  5. 原来的target和subscript,从第1步开始就在栈上耐心等待,然后用它来进行下标存储(第16行和18行,下标还是在计算的下标第 2 行,-1。但此时 a[-1] 指的是 a 的另一个元素。 需要轮换才能将 for 的参数按正确的顺序排列。因为赋值的正常求值顺序是先求 right-hand 端,所以虚拟机假定新值位于栈底,然后是对象及其下标。

  6. 最后,None作为语句的值返回。

assignment and augmented assignment statements are documented in the Python reference manual. Another important source of information is the description of the __iadd__ special method. Evaluation (and evaluation order) for augmented assignment operations is sufficiently confusing that there is a Programming FAQ的精确工作原理,如果你想了解确切的机制,值得仔细阅读。

尽管这些信息很有趣,但值得补充的是编写依赖于细节的程序扩充赋值中的评估顺序不利于生成可读代码。在几乎所有情况下,都应避免依赖于 non-obvious 过程细节的扩充赋值,包括诸如作为此问题目标的语句之类的语句。