什么时候“ i + = x”与Python中的“ i = i + x”不同?


212

有人告诉我它+=可能会与标准符号产生不同的影响i = i +。是否有与以下情况i += 1不同的情况i = i + 1


7
+=就像extend()列表一样。
Ashwini Chaudhary 2013年

12
@AshwiniChaudhary这是一个非常微妙的区别,考虑到i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]True。许多开发人员可能不会注意到id(i)对一项操作的更改,而对另一项操作则没有。
kojiro

1
@kojiro-虽然这是一个微妙的区别,但我认为这是一个重要的区别。
mgilson

@mgilson这很重要,因此我觉得需要解释。:)
kojiro

1
有关两者在Java中的区别的相关问题:stackoverflow.com/a/7456548/245966
jakub.g 2013年

Answers:


317

这完全取决于对象i

+=调用__iadd__方法(如果存在- __add__如果不存在则返回),而+调用__add__方法1或在__radd__某些情况下调用方法2

从API的角度来看,__iadd__应该将其用于就地修改可变对象(返回已变异的对象),而__add__应该返回某些东西的新实例。对于不可变的对象,这两种方法都返回一个新实例,但是__iadd__会将新实例放置在当前名称空间中,其名称与旧实例的名称相同。这就是为什么

i = 1
i += 1

似乎在增加i。实际上,您将获得一个新的整数并将其分配给“顶部” i-丢失对旧整数的一个引用。在这种情况下,i += 1与完全相同i = i + 1。但是,对于大多数易变的物体,情况就不同了:

作为一个具体的例子:

a = [1, 2, 3]
b = a
b += [1, 2, 3]
print a  #[1, 2, 3, 1, 2, 3]
print b  #[1, 2, 3, 1, 2, 3]

相比:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

请注意,在第一个示例中,由于ba引用相同的对象,因此当我+=在on上使用时b,它实际上发生了变化b(并且也a看到了这种变化-毕竟,它引用了相同的列表)。但是,在第二种情况下,当我这样做时b = b + [1, 2, 3],它将采用b引用的列表并将其与新列表连接起来[1, 2, 3]。然后,它将串联的列表存储为b-而不考虑b之前的行。


1在表达式中x + y,如果x.__add__未实现或如果x.__add__(y)返回NotImplemented 并且 xy具有不同的类型,则x + y尝试调用y.__radd__(x)。所以,如果你有

foo_instance += bar_instance

如果Foo没有实现__add____iadd__那么这里的结果与

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2在表达式中foo_instance + bar_instance如果的类型是(例如)类型的子类,bar_instance.__radd__则将在之前尝试。合理的,这是因为在某种意义上是一种“更高级”的对象不是那么应该得到压倒一切的选项的行为。foo_instance.__add__ bar_instancefoo_instanceissubclass(Bar, Foo)BarFooBarFoo


18
好吧,如果存在则+=调用,否则返回添加和重新绑定。这就是为什么即使没有也能正常工作的原因。但是除了那次小巧的话,还有很多解释。__iadd__ i = 1; i += 1int.__iadd__
abarnert 2013年

4
@abarnert-我一直以为int.__iadd__只是调用了__add__。我很高兴今天学到了一些新东西:)。
mgilson

@abarnert-我想可能是完整的,如果不存在(或返回and 和属于不同类型),则进行x + y呼叫y.__radd__(x)x.__add__NotImplementedxy
mgilson

如果您真的想成为一名程序员,则必须提到“如果存在”位通过常规的getattr机制进行处理,除了一些带有经典类的怪癖之外,对于在C API中实现的类型,它会查找nb_inplace_addsq_inplace_concat,并且这些C API函数比Python dunder方法具有更严格的要求,并且…但是我认为这与答案无关。主要区别在于,+=尝试先进行就地添加,然后再回退为like +,我想您已经解释了。
abarnert 2013年

是的,我想您是对的...尽管我可以回避C API不属于python的立场。它是Cpython :-P 的一部分
mgilson 2013年

67

在幕后,i += 1执行以下操作:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

虽然i = i + 1做了这样的事情:

i = i.__add__(1)

这有点过分简化,但您会明白:Python +=通过创建__iadd__方法和,为类型提供了一种专门处理类型的方法__add__

目的是使可变类型(如list)将自身变异__iadd__(然后返回self,除非您做的非常棘手),而可变类型(如int将不会实现。

例如:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

因为和l2是同一对象l1,并且您进行了突变l1,所以您也进行了突变l2

但:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

在这里,你没有变异l1;相反,您创建了一个新列表,l1 + [3]然后反弹该名称l1l2指向该列表,而指向原始列表。

(在该+=版本中,您还需要重新绑定l1,只是在这种情况下,您需要将list其重新绑定到已经绑定的相同的位置,因此通常可以忽略该部分。)


没有__iadd__实际调用__add__中的事件AttributeError
mgilson

好吧,i.__iadd__不打电话__add__;就是i += 1那个__add__
abarnert

嗯...那是我的意思。有趣。我没有意识到那是自动完成的。
mgilson

3
第一次尝试实际上是i = i.__iadd__(1)- iadd 可以在适当位置修改对象,但不必这样做,因此在两种情况下都希望返回结果。
lvc

请注意,这意味着operator.iadd电话__add__AttributeError,但它不能重新绑定结果......所以i=1; operator.iadd(i, 1)回报2和叶i设置为1。这有点令人困惑。
abarnert 2013年

6

下面是比较直接的例子i += xi = i + x

def foo(x):
  x = x + [42]

def bar(x):
  x += [42]

c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.