尝试了解元组中列表的 += 和 .append() 方法之间的差异。提出这个问题是为了了解“内部厨房”,而不是为了寻找实际应用。示例代码:
def add_func():
a = (1, 2, [1, 2])
a[-1] += [3] # в список значение добавится, но потом падает ошибка
第二个版本:
def append_func():
a = (1, 2, [1, 2])
a[-1].append(3) # в данном случае всё корректно отработает
查看字节码后,我发现有些行有所不同:
print(dis.dis(add_func))
print('--------------')
print(dis.dis(append_func))
4 0 LOAD_CONST 1 (1)
2 LOAD_CONST 2 (2)
4 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
8 BUILD_LIST 2
10 BUILD_TUPLE 3
12 STORE_FAST 0 (a)
5 14 LOAD_FAST 0 (a)
16 LOAD_CONST 3 (-1)
18 DUP_TOP_TWO
20 BINARY_SUBSCR
22 LOAD_CONST 4 (3)
24 BUILD_LIST 1
26 INPLACE_ADD # Кажется, это "отголоски" __iadd__
28 ROT_THREE
30 STORE_SUBSCR
32 LOAD_CONST 0 (None)
34 RETURN_VALUE
--------------
8 0 LOAD_CONST 1 (1)
2 LOAD_CONST 2 (2)
4 LOAD_CONST 1 (1)
6 LOAD_CONST 2 (2)
8 BUILD_LIST 2
10 BUILD_TUPLE 3
12 STORE_FAST 0 (a)
9 14 LOAD_FAST 0 (a)
16 LOAD_CONST 3 (-1)
18 BINARY_SUBSCR
20 LOAD_METHOD 0 (append)
22 LOAD_CONST 4 (3)
24 CALL_METHOD 1
26 POP_TOP
28 LOAD_CONST 0 (None)
30 RETURN_VALUE
字节码的差异让我认为我理解了这个问题,但在那之后我决定阅读SO。
引用这个问题:
+= 是一个赋值。当你使用它时,你实际上是在说“some_list2= some_list2+['something']”。分配涉及重新绑定,因此:
同时他们__iadd__ 在这里写到:
从 API 的角度来看,iadd应该用于就地修改可变对象(返回已变异的对象),而add应该返回某个新的实例。
还有L. Ramalho 的书《Python - to the heights of mastery》中的一句话:
如果左侧的变量与不可变对象关联,并且可以就地修改可变对象,则复合赋值(运算符 +=、*= 等)会创建一个新对象。
坦白说,我对此感到困惑,并发现其中有很多矛盾。如果+=“就地”更改对象,为什么会发生错误?
PS我也不太理解STORE_FAST文档中字节码中的一行:
将 STACK.pop() 存储到本地 co_varnames[var_num] 中。
这是语言缺陷。Python中的构造
x += y统一处理如下:第一步是访问对象
x并要求它对对象执行某些操作y。作为此调用的结果,返回了某个对象z,该对象最终被写入x.让我们比较一下
__iadd__列表和元组的实现。在第一种情况下,列表会修改自身并返回自身。在第二种情况下,这是行不通的——元组是不可变的。我们能做的最好的事情就是将内容复制
self到y一个新的元组中并返回它。列表操作具有更好的复杂性——摊余常数。对元组的操作具有线性复杂度。速度上的差异证明了不同实现的合理性。翻译代码时,
x += yPython 编译器必须创建能够正确处理列表和元组的代码。对于元组来说,x = z需要赋值,否则运算结果将无法到达程序员手中。对于列表来说,此分配是无害的。几乎无害。如果左边
+=有一个不能改变的结构怎么办,比如元组a[-1]在哪里?a然后,当程序执行时,会调用tuple方法__setitem__。但它总是失败并出现错误 - 你无法更改元组。语言作者希望使列表和元组的使用具有多态性,从而导致了这种情况。顺便说一下,如果不改变语言就无法纠正这个问题。
编译器行为可以改进。
第一个想法:如果元组是自分配的,则允许对其进行分配。事实上,在列表的情况下,运算符
x = z实际上会说x = x。在__setitem__元组方法中,可以捕获自分配并返回控制而不会出现错误。第二个想法:在评估右侧之前检查左侧的构造
+=是否可分配。这将需要更改语言,但也许这种更改可以向后兼容。第三个想法:如果不需要,就不要调用赋值。例如这样:
我认为没有人会纠正这个小缺陷。这甚至不是一个缺陷,而是一个考虑我们为代码在对其处理的值类型知之甚少的情况下工作而预先付出多少代价的理由。
考虑操作的字节码
s[a] += b:s[a]入栈顶;+=) - 此操作会成功,因为它引用了一个可变对象;bs[a]s[a]s[a])执行分配以s[a]- 此操作将失败,因为s- 不可变对象(元组)。同时,在第二个例子中:
没有赋值,只是调用元组元素之一的方法,并且不会出现错误 - 毕竟,元组仅存储对对象的引用,并且当对象更改时引用不会更改。在第一个示例中,尝试保存新链接而不是旧链接,这导致了错误。