print(*a, a.pop(0))的翻译是什么意思?

64

这段代码:

a = [1, 2, 3]
print(*a, a.pop(0))

Python 3.8 在拆包前打印 2 3 1(在执行pop之前)。
Python 3.9 在拆包后打印 1 2 3 1(在执行pop之后)。

导致这种变化的原因是什么?我在更改日志中没有找到相关的信息。

编辑:不仅在函数调用中,例如在列表显示中也适用:

a = [1, 2, 3]
b = [*a, a.pop(0)]
print(b)
打印[2, 3, 1]与 [1, 2, 3, 1]。并且 Expression lists 中说:“从左到右评估表达式”(这是Python 3.8文档的链接),因此我希望拆包表达式先发生。

9
这原本就是被定义或保证的行为吗? - deceze
4
如果没有对语言进行任何更改,那么它应该能够给出一致的结果。 - Elder Yeager
16
不一定。如果这是未定义的,你就不应该在第一时间使用它。 - blue_note
2
有点相关 Python 的函数参数和操作数的求值顺序是确定的吗?(+ 在哪里记录)? - 请注意 @wim 关于错误的第二条评论。 - buran
2
@CodyGray 我想这取决于人们对"未定义行为"的理解,但也许像set元素的顺序,文档中称为"CPython实现细节"的大部分内容,或者在搜索"undefined"时发现的一些内容都属于此类。 - Kelly Bundy
显示剩余7条评论
1个回答

53
我怀疑这可能是一个意外,尽管我更喜欢新行为。 新行为是由于对*参数的字节码处理方式进行了更改而导致的。 更改在Python 3.9.0 alpha 3的更新日志中有说明:

bpo-39320: 用三个更简单的代码替换四个用于构建序列的复杂字节码。

以下四个字节码已被删除:

  • BUILD_LIST_UNPACK
  • BUILD_TUPLE_UNPACK
  • BUILD_SET_UNPACK
  • BUILD_TUPLE_UNPACK_WITH_CALL

以下三个字节码已添加:

  • LIST_TO_TUPLE
  • LIST_EXTEND
  • SET_UPDATE
在Python 3.8中,f(*a, a.pop())的字节码如下:
  1           0 LOAD_NAME                0 (f)
              2 LOAD_NAME                1 (a)
              4 LOAD_NAME                1 (a)
              6 LOAD_METHOD              2 (pop)
              8 CALL_METHOD              0
             10 BUILD_TUPLE              1
             12 BUILD_TUPLE_UNPACK_WITH_CALL     2
             14 CALL_FUNCTION_EX         0
             16 RETURN_VALUE

在3.9上,它看起来像这样:

  1           0 LOAD_NAME                0 (f)
              2 BUILD_LIST               0
              4 LOAD_NAME                1 (a)
              6 LIST_EXTEND              1
              8 LOAD_NAME                1 (a)
             10 LOAD_METHOD              2 (pop)
             12 CALL_METHOD              0
             14 LIST_APPEND              1
             16 LIST_TO_TUPLE
             18 CALL_FUNCTION_EX         0
             20 RETURN_VALUE
在旧的字节码中,代码将 a(a.pop(),) 推入堆栈,然后将这两个可迭代对象解包成一个元组。在新的字节码中,代码将一个列表压入堆栈,然后执行 l.extend(a)l.append(a.pop()),最后调用tuple(l)。这种更改的效果是在 pop 调用之前移位了对 a 的解包,但似乎这并不是有意为之的。查看bpo-39320,意图是简化字节码指令,而不是更改行为,该 bpo 线程没有讨论行为变化。

链接的答案表明顺序是从左到右,并且还给出了示例 expr1(expr2, expr3, *expr4, **expr5)。因此,对于 Python 3.8 版本之前似乎不正确,对吗?如果两者顺序相反,print(a.pop(0), *a),在 3.9 中会发生什么? - ilkkachu
1
@ilkkachu:表达式从左到右进行评估,但*expr4不是表达式。expr4是。*是函数调用语法的一部分,而评估顺序文档并没有承诺何时进行解包。 - user2357112
2
关于 print(a.pop(0), *a),不论版本如何,pop 都会在解包之前发生。 - user2357112
@user2357112支持Monica "*a不是表达式"。 - 我不确定这个论点是否站得住脚。如果我在Python 3.9中输入一个裸的*a,我会得到SyntaxError: can't use starred expression here,所以显然Python认为它是某种表达式。它在每个上下文中都不能容忍这一事实并不改变它是表达式的事实。 - marcelm
3
“星号表达式”这个术语可能只是指被加上星号的表达式,它并不意味着加星号后得到另一个表达式。例如,可以比较一下“截断词”的术语,它并不一定意味着像 floweflower 的截断形式)这样的内容本身就是一个单词。 - kaya3
@marcelm,嗯,实际上它不是(一个普通的)表达式,这是有道理的。它在于产生多个值,但本身不是列表、元组或其他什么东西。虽然我认为从左到右的顺序也应该包括解包或任何其他需要生成实际传递的参数的操作。 - ilkkachu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接