Quand «i + = x» est-il différent de «i = i + x» en Python?

212

On m'a dit que cela +=peut avoir des effets différents de la notation standard de i = i +. Y a-t-il un cas dans lequel i += 1serait différent i = i + 1?

MarJamRob
la source
7
+=agit comme extend()en cas de listes.
Ashwini Chaudhary
12
Ce @AshwiniChaudhary est une distinction assez subtile, étant donné que i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]c'est True. De nombreux développeurs peuvent ne pas remarquer que les id(i)changements pour une opération, mais pas pour l'autre.
kojiro
1
@kojiro - Bien que ce soit une distinction subtile, je pense qu'elle est importante.
mgilson
@mgilson c'est important, et j'ai donc pensé qu'il fallait une explication. :)
kojiro
1
Question connexe concernant les différences entre les deux en Java: stackoverflow.com/a/7456548/245966
jakub.g

Réponses:

317

Cela dépend entièrement de l'objet i.

+=appelle la __iadd__méthode (si elle existe - se replie __add__si elle n'existe pas) alors qu'elle +appelle la __add__méthode 1 ou la __radd__méthode dans quelques cas 2 .

Du point de vue de l'API, __iadd__est censé être utilisé pour modifier les objets mutables en place (renvoyer l'objet qui a été muté) alors qu'il __add__devrait renvoyer une nouvelle instance de quelque chose. Pour les objets immuables , les deux méthodes renvoient une nouvelle instance, mais __iadd__placent la nouvelle instance dans l'espace de noms actuel avec le même nom que l'ancienne instance. C'est pourquoi

i = 1
i += 1

semble augmenter i. En réalité, vous obtenez un nouvel entier et l'assignez "au-dessus de" i- perdant une référence à l'ancien entier. Dans ce cas, i += 1est exactement le même que i = i + 1. Mais, avec la plupart des objets mutables, c'est une autre histoire:

À titre d'exemple concret:

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]

par rapport à:

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

Remarquez comment dans le premier exemple, depuis bet aréférence le même objet, quand je l' utilise +=sur b, il change réellement b(et avoit ce changement trop - Après tout, il est référence à la même liste). Dans le deuxième cas cependant, quand je le fais b = b + [1, 2, 3], cela prend la liste qui bfait référence et la concatène avec une nouvelle liste [1, 2, 3]. Il stocke ensuite la liste concaténée dans l'espace de noms actuel en tant que b- Sans tenir compte de ce qu'était bla ligne avant.


1 Dans l'expression x + y, si x.__add__n'est pas implémenté ou si x.__add__(y)retourne NotImplemented et xet ya différents types , x + yessaie alors d'appeler y.__radd__(x). Donc, dans le cas où vous avez

foo_instance += bar_instance

si Foone met pas en œuvre __add__ou __iadd__alors le résultat ici est le même que

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 Dans l'expression foo_instance + bar_instance, bar_instance.__radd__sera essayé avant foo_instance.__add__ si le type de bar_instanceest une sous-classe du type de foo_instance(par exemple issubclass(Bar, Foo)). La raison en est que, Bardans un certain sens, c'est un objet de "niveau supérieur" Fooqui Bardevrait donc avoir la possibilité de remplacer Foole comportement de.

mgilson
la source
18
Eh bien, +=appelle __iadd__ s'il existe , et revient à ajouter et à relier autrement. C'est pourquoi ça i = 1; i += 1marche même s'il n'y en a pas int.__iadd__. Mais à part ce petit détail, de grandes explications.
abarnert
4
@abarnert - J'ai toujours supposé que je int.__iadd__venais d'appeler __add__. Je suis content d'avoir appris quelque chose de nouveau aujourd'hui :).
mgilson
@abarnert - Je suppose que peut-être pour être complet , x + yappelle y.__radd__(x)si x.__add__n'existe pas (ou retourne NotImplementedet xet ysont de types différents)
mgilson
Si vous voulez vraiment être completiste, vous devez mentionner que le bit "s'il existe" passe par les mécanismes getattr habituels, à l'exception de certaines bizarreries avec des classes classiques, et pour les types implémentés dans l'API C, il recherche plutôt nb_inplace_addou sq_inplace_concat, et ces fonctions de l'API C ont des exigences plus strictes que les méthodes Python dunder, et… Mais je ne pense pas que ce soit pertinent pour la réponse. La principale distinction est que l'on +=essaie de faire un ajout sur place avant de retomber dans le comportement +, ce que je pense que vous avez déjà expliqué.
abarnert
Ouais, je suppose que vous avez raison ... Bien que je puisse simplement me rabattre sur le fait que l'API C ne fait pas partie de python . Cela fait partie de Cpython :-P
mgilson
67

Sous les couvertures, i += 1fait quelque chose comme ceci:

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

Alors i = i + 1fait quelque chose comme ça:

i = i.__add__(1)

Il s'agit d'une légère simplification excessive, mais vous avez l'idée: Python donne aux types un moyen de gérer +=spécialement, en créant une __iadd__méthode ainsi qu'un __add__.

L'intention est que les types mutables, comme list, se mutent __iadd__(et reviennent ensuite self, sauf si vous faites quelque chose de très délicat), tandis que les types immuables, comme int, ne l'implémentent tout simplement pas.

Par exemple:

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

Parce que l2c'est le même objet que l1, et vous avez muté l1, vous avez également muté l2.

Mais:

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

Ici, vous n'avez pas mué l1; au lieu de cela, vous avez créé une nouvelle liste l1 + [3]et rebondissez le nom l1pour le pointer, en laissant l2pointer vers la liste d'origine.

(Dans la +=version, vous étiez également en train de relier l1, c'est juste que dans ce cas, vous le liiez à celui auquel listil était déjà lié, vous pouvez donc généralement ignorer cette partie.)

abarnert
la source
ne __iadd__fait appeler __add__en cas d'un AttributeError?
mgilson
Eh bien, i.__iadd__n'appelle pas __add__; c'est ça i += 1qui appelle __add__.
abarnert
euh ... Ouais, c'est ce que je voulais dire. Intéressant. Je ne savais pas que cela se faisait automatiquement.
mgilson
3
La première tentative est en fait i = i.__iadd__(1)- iadd peut modifier l'objet en place, mais n'est pas obligée, et devrait donc retourner le résultat dans les deux cas.
lvc
Notez que cela signifie que les operator.iaddappels __add__sur AttributeError, mais il ne peut pas reconsolider le résultat ... donc i=1; operator.iadd(i, 1)renvoie 2 et les feuilles ifixées à 1. Ce qui est un peu déroutant.
abarnert
6

Voici un exemple qui se compare directement i += xà 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]
Deqing
la source