当b是一个列表时,为什么b += (4,)有效而b = b + (4,)无效?

78
如果我们取b = [1,2,3],并尝试执行:b+=(4,) 它会返回b = [1,2,3,4],但是如果我们尝试执行b = b + (4,),它将不起作用。
b = [1,2,3]
b+=(4,) # Prints out b = [1,2,3,4]
b = b + (4,) # Gives an error saying you can't add tuples and lists

我本以为b+=(4,)会失败,因为你不能将列表和元组相加,但它却起作用了。所以我尝试使用b = b + (4,),期望得到相同的结果,但它没有起作用。


4
我相信可以在这里找到答案:https://dev59.com/YGYr5IYBdhLWcg3wKHIp。 - jochen
1
为什么我无法在Python中使用+运算符将元组添加到列表中? - splash58
一开始我误读了问题并试图关闭它,认为它太过笼统,但后来又撤回了这一决定。然后我认为它可能是一个重复的问题,但我既无法重新投票,也找不到其他类似的答案,让我非常苦恼。 :/ - Karl Knechtel
非常类似的问题:https://stackoverflow.com/questions/58048664/understanding-pythons-builtins-operator-overloading-behavior - sanyassh
这个回答解决了你的问题吗?list.__iadd__和list.__add__的不同行为 - Pythoneer
同样的问题:https://dev59.com/Hmkw5IYBdhLWcg3wm74h - Pythoneer
7个回答

72

“为什么”问题的问题在于通常它们可能有不止一个含义。我会尝试回答您可能想到的每个问题。

“为什么可能会有不同的工作方式?” 可以通过例如这个来解答。基本上,+= 尝试使用对象的不同方法: __iadd__ (仅在左边检查),对于 +,则是 __add____radd__(如果左侧没有 __add__ 则在右侧检查“反向添加”)

“每个版本究竟做了什么?” 简单地说,list.__iadd__ 方法和 list.extend 做的事情是相同的(但由于语言设计,仍然需要赋值回去)。

这也意味着例如

>>> a = [1,2,3]
>>> b = a
>>> a += [4] # uses the .extend logic, so it is still the same object
>>> b # therefore a and b are still the same list, and b has the `4` added
[1, 2, 3, 4]
>>> b = b + [5] # makes a new list and assigns back to b
>>> a # so now a is a separate list and does not have the `5`
[1, 2, 3, 4]

+符号创建一个新对象,但需要明确另一个列表并将其添加到该对象中,而不是尝试从不同的序列中提取元素。

"为什么使用+=这种方式很有用?因为它更有效率; extend方法不必创建一个新对象。当然,有时会产生一些令人惊讶的影响(如上所述),总体而言,Python并不完全关注效率,但这些决策是在很长时间以前做出的。

"为什么不允许使用+来添加列表和元组?"请参见此处(感谢@splash58); 其中之一的想法是(tuple + list)应该产生与(list + tuple)相同的类型,而结果应该是哪种类型并不清楚。+=则没有这个问题,因为a+=b显然不应该改变a的类型。


2
哦,非常正确。而且列表不使用“|”,所以这有点破坏了我的示例。如果我以后想到更清晰的例子,我会把它换掉。 - Karl Knechtel
1
顺便提一下,对于集合来说,| 是可交换的运算符,但对于列表来说,+ 不是。因此,我认为关于类型模糊的论点并不特别强。既然运算符不可交换,为什么要求类型相同呢?我们可以约定结果具有左侧的类型。另一方面,通过限制 list + iterator,开发人员被鼓励更加明确地表达他们的意图。如果您想创建一个包含从 a 扩展的内容和从 b 扩展的内容的新列表,已经有一种方法可以实现:new = a.copy(); new += b。这只是多了一行代码,但非常清晰明了。 - a_guest
a += ba = a + b 行为不同的原因并不是效率问题。实际上,这是因为 Guido 认为所选择的行为“更少”令人困惑。想象一下一个函数接收一个列表 a 作为参数,然后执行 a += [1, 2, 3]。这个语法显然看起来像是在原地修改列表,而不是创建一个新列表,因此决定让它按照大多数人对预期结果的直觉行事。然而,该机制也必须适用于像 int 这样的不可变类型,这导致了当前的设计。 - Sven Marnach
我个人认为,与 Ruby 所做的将 a += b 简写为 a = a + b 相比,这种设计实际上更加令人困惑,但我可以理解我们是如何到达这里的。 - Sven Marnach

22

它们不是等价的:

b += (4,)

是以下缩写:

b.extend((4,))

+ 用于连接列表时,因此可以通过以下方式:

b = b + (4,)

您正在尝试将元组连接到列表中


15

当你这样做时:

b += (4,)

被转换为:

b.__iadd__((4,)) 

在幕后,它调用b.extend((4,))extend接受一个迭代器,这就是为什么这也可以工作的原因:

b = [1,2,3]
b += range(2)  # prints [1, 2, 3, 0, 1]

但是当你这样做时:

b = b + (4,)

被转换为:

b = b.__add__((4,)) 

只接受列表对象。


4

根据官方文档,对于可变序列类型,两者均为:

s += t
s.extend(t)

被定义为:

t的内容扩展到s

这与以下定义不同:

s = s + t    # not equivalent in Python!

这也意味着任何序列类型都可以用于t,包括元组,就像您的示例一样。
但它也适用于范围和生成器!例如,您还可以执行以下操作:
s += range(3)

3
"augmented"赋值运算符,例如 += ,是在2000年10月发布的Python 2.0中引入的。设计和原理在PEP 203中描述。这些运算符声明的目标之一是支持原地操作。写作:
a = [1, 2, 3]
a += [4, 5, 6]

这个操作理应在原地更新列表a。如果有其他引用了列表a的情况,比如其作为函数参数传递时,这就很重要。

然而,并非所有情况下都能在原地执行该操作,因为许多 Python 类型,包括整数和字符串,是不可变的。因此,对于整数i,例如 i += 1 就无法在原地执行。

总之,增强赋值运算符应当尽可能在原地执行,否则将创建新对象。为实现这些设计目标,规定表达式 x += y 的行为如下:

  • 如果定义了 x.__iadd__ 方法,则执行 x.__iadd__(y)
  • 否则,如果实现了 x.__add__ 方法,则执行 x.__add__(y)
  • 否则,如果实现了 y.__radd__ 方法,则执行 y.__radd__(x)
  • 否则抛出错误。

通过这一过程获得的第一个结果将被重新赋值给 x(除非该结果是 NotImplemented 单例,在这种情况下查找继续进行下一步)。

这个过程允许支持原地修改的类型实现 __iadd__()。不支持原地修改的类型不需要添加任何新的魔法方法,因为 Python 会自动退化为基本操作 x = x + y

那么最后来到你真正的问题——为什么可以使用增强赋值运算符将元组加入列表。从我的记忆中,这个历史大致如下:在 Python 2.0 中,list.__iadd__() 方法被实现为简单地调用已经存在的 list.extend() 方法。当迭代器被引入 Python 2.1 时,list.extend() 方法被更新以接受任意迭代器。这些变化的最终结果是,my_list += my_tuple 在 Python 2.1 起开始工作了。然而,list.__add__() 方法从未预期支持任意迭代器作为右侧参数——这被认为在一种强类型语言中是不合适的。

我个人认为 Python 中的增强赋值运算符实现得有点太复杂了。它有很多出人意料的副作用,例如以下代码:

t = ([42], [43])
t[0] += [44]

第二行代码会抛出“TypeError: 'tuple' object does not support item assignment”异常,但操作仍然成功执行 - 执行引发错误的那行代码后,变量t将会是“( [42, 44],[43])”。参考资料

太棒了!拥有PEP的参考资料特别有用。我在另一端添加了一个链接,指向之前关于元组中列表行为的SO问题。当我回顾Python 2.3之前的版本时,与今天相比,它似乎几乎无法使用...(我仍然模糊地记得尝试并未能让1.5在非常老的Mac上做任何有用的事情) - Karl Knechtel

2

大多数人会认为 X += Y 等同于 X = X + Y。事实上,在Mark Lutz的Python Pocket Reference(第4版)第57页中写道:“以下两种格式大致等效:X = X + Y,X += Y”。然而,指定Python的人并没有使它们等价。可能这是一个错误,将导致沮丧的程序员花费数小时进行调试,只要Python仍在使用,但现在它就是Python的方式。如果X是可变序列类型,则 X += Y 等同于 X.extend( Y ) 而不是 X = X + Y。


也许那是一个错误,会导致无数沮丧的程序员在使用Python的时间里花费数小时进行调试。你是否真的因此而遭受了痛苦?你似乎是在从经验中谈论这个问题。我非常想听听你的故事。 - Veky

1

此处所述,如果array没有实现__iadd__方法,则b+=(4,)将仅仅是b = b + (4,)的简写,但显然它并不是。因此,array确实实现了__iadd__方法。显然,__iadd__方法的实现类似于以下内容:

def __iadd__(self, x):
    self.extend(x)

然而,我们知道上述代码并不是__iadd__方法的实际实现,但我们可以假设并接受存在类似于extend方法的东西,它接受tupple输入。

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