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

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

I was told that += can have different effects than the standard notation of i = i +. Is there a case in which i += 1 would be different from i = i + 1?


回答 0

这完全取决于对象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

This depends entirely on the object i.

+= calls the __iadd__ method (if it exists — falling back on __add__ if it doesn’t exist) whereas + calls the __add__ method1 or the __radd__ method in a few cases2.

From an API perspective, __iadd__ is supposed to be used for modifying mutable objects in place (returning the object which was mutated) whereas __add__ should return a new instance of something. For immutable objects, both methods return a new instance, but __iadd__ will put the new instance in the current namespace with the same name that the old instance had. This is why

i = 1
i += 1

seems to increment i. In reality, you get a new integer and assign it “on top of” i — losing one reference to the old integer. In this case, i += 1 is exactly the same as i = i + 1. But, with most mutable objects, it’s a different story:

As a concrete example:

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]

compared to:

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

notice how in the first example, since b and a reference the same object, when I use += on b, it actually changes b (and a sees that change too — After all, it’s referencing the same list). In the second case however, when I do b = b + [1, 2, 3], this takes the list that b is referencing and concatenates it with a new list [1, 2, 3]. It then stores the concatenated list in the current namespace as b — With no regard for what b was the line before.


1In the expression x + y, if x.__add__ isn’t implemented or if x.__add__(y) returns NotImplemented and x and y have different types, then x + y tries to call y.__radd__(x). So, in the case where you have

foo_instance += bar_instance

if Foo doesn’t implement __add__ or __iadd__ then the result here is the same as

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2In the expression foo_instance + bar_instance, bar_instance.__radd__ will be tried before foo_instance.__add__ if the type of bar_instance is a subclass of the type of foo_instance (e.g. issubclass(Bar, Foo)). The rationale for this is that Bar is in some sense a “higher-level” object than Foo so Bar should get the option of overriding Foo‘s behavior.


回答 1

在幕后,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其重新绑定到已经绑定的相同的位置,因此通常可以忽略该部分。)

Under the covers, i += 1 does something like this:

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

While i = i + 1 does something like this:

i = i.__add__(1)

This is a slight oversimplification, but you get the idea: Python gives types a way to handle += specially, by creating an __iadd__ method as well as an __add__.

The intention is that mutable types, like list, will mutate themselves in __iadd__ (and then return self, unless you’re doing something very tricky), while immutable types, like int, will just not implement it.

For example:

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

Because l2 is the same object as l1, and you mutated l1, you also mutated l2.

But:

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

Here, you didn’t mutate l1; instead, you created a new list, l1 + [3], and rebound the name l1 to point at it, leaving l2 pointing at the original list.

(In the += version, you were also rebinding l1, it’s just that in that case you were rebinding it to the same list it was already bound to, so you can usually ignore that part.)


回答 2

下面是比较直接的例子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]

Here is an example that directly compares i += x with i = 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]

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。