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?
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. :)
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 NotImplementedet 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
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.
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)exceptAttributeError:
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.)
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)- iaddpeut 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]
+=
agit commeextend()
en cas de listes.i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]
c'estTrue
. De nombreux développeurs peuvent ne pas remarquer que lesid(i)
changements pour une opération, mais pas pour l'autre.Réponses:
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 pourquoisemble 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 += 1
est exactement le même quei = i + 1
. Mais, avec la plupart des objets mutables, c'est une autre histoire:À titre d'exemple concret:
par rapport à:
Remarquez comment dans le premier exemple, depuis
b
eta
référence le même objet, quand je l' utilise+=
surb
, il change réellementb
(eta
voit ce changement trop - Après tout, il est référence à la même liste). Dans le deuxième cas cependant, quand je le faisb = b + [1, 2, 3]
, cela prend la liste quib
fait 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 queb
- Sans tenir compte de ce qu'étaitb
la ligne avant.1 Dans l'expression
x + y
, six.__add__
n'est pas implémenté ou six.__add__(y)
retourneNotImplemented
etx
ety
a différents types ,x + y
essaie alors d'appelery.__radd__(x)
. Donc, dans le cas où vous avezfoo_instance += bar_instance
si
Foo
ne met pas en œuvre__add__
ou__iadd__
alors le résultat ici est le même quefoo_instance = bar_instance.__radd__(bar_instance, foo_instance)
2 Dans l'expression
foo_instance + bar_instance
,bar_instance.__radd__
sera essayé avantfoo_instance.__add__
si le type debar_instance
est une sous-classe du type defoo_instance
(par exempleissubclass(Bar, Foo)
). La raison en est que,Bar
dans un certain sens, c'est un objet de "niveau supérieur"Foo
quiBar
devrait donc avoir la possibilité de remplacerFoo
le comportement de.la source
+=
appelle__iadd__
s'il existe , et revient à ajouter et à relier autrement. C'est pourquoi çai = 1; i += 1
marche même s'il n'y en a pasint.__iadd__
. Mais à part ce petit détail, de grandes explications.int.__iadd__
venais d'appeler__add__
. Je suis content d'avoir appris quelque chose de nouveau aujourd'hui :).x + y
appelley.__radd__(x)
six.__add__
n'existe pas (ou retourneNotImplemented
etx
ety
sont de types différents)nb_inplace_add
ousq_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é.Sous les couvertures,
i += 1
fait quelque chose comme ceci:Alors
i = i + 1
fait quelque chose comme ça: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 ensuiteself
, sauf si vous faites quelque chose de très délicat), tandis que les types immuables, commeint
, ne l'implémentent tout simplement pas.Par exemple:
Parce que
l2
c'est le même objet quel1
, et vous avez mutél1
, vous avez également mutél2
.Mais:
Ici, vous n'avez pas mué
l1
; au lieu de cela, vous avez créé une nouvelle listel1 + [3]
et rebondissez le noml1
pour le pointer, en laissantl2
pointer vers la liste d'origine.(Dans la
+=
version, vous étiez également en train de relierl1
, c'est juste que dans ce cas, vous le liiez à celui auquellist
il était déjà lié, vous pouvez donc généralement ignorer cette partie.)la source
__iadd__
fait appeler__add__
en cas d'unAttributeError
?i.__iadd__
n'appelle pas__add__
; c'est çai += 1
qui appelle__add__
.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.operator.iadd
appels__add__
surAttributeError
, mais il ne peut pas reconsolider le résultat ... donci=1; operator.iadd(i, 1)
renvoie 2 et les feuillesi
fixées à1
. Ce qui est un peu déroutant.Voici un exemple qui se compare directement
i += x
ài = i + x
:la source