Pourquoi + = se comporte-t-il de manière inattendue sur les listes?

118

L' +=opérateur en python semble fonctionner de manière inattendue sur les listes. Quelqu'un peut-il me dire ce qui se passe ici?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

PRODUCTION

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += barsemble affecter chaque instance de la classe, alors que foo = foo + barsemble se comporter de la manière dont je m'attendrais à ce que les choses se comportent.

L' +=opérateur est appelé "opérateur d'affectation composé".

eucalculie
la source
voir la différence entre «étendre» et «ajouter» sur la liste aussi
N 1.1
3
Je ne pense pas que cela montre quelque chose de mal avec Python. La plupart des langages ne vous permettraient même pas d'utiliser l' +opérateur sur des tableaux. Je pense qu'il est tout à fait logique dans ce cas d' +=ajouter.
Skilldrick
4
C'est ce qu'on appelle une «affectation augmentée», officiellement.
Martijn Pieters

Réponses:

138

La réponse générale est que +=tente d'appeler la __iadd__méthode spéciale, et si elle n'est pas disponible, elle essaie de l'utiliser à la __add__place. Le problème réside donc dans la différence entre ces méthodes spéciales.

La __iadd__méthode spéciale est pour un ajout sur place, c'est-à-dire qu'elle mute l'objet sur lequel elle agit. La __add__méthode spéciale renvoie un nouvel objet et est également utilisée pour l' +opérateur standard .

Ainsi, lorsque l' +=opérateur est utilisé sur un objet qui a un __iadd__défini l'objet est modifié en place. Sinon, il essaiera d'utiliser le plain __add__et de renvoyer un nouvel objet.

C'est pourquoi pour les types mutables comme les listes +=modifie la valeur de l'objet, alors que pour les types immuables comme les tuples, les chaînes et les entiers, un nouvel objet est renvoyé à la place ( a += bdevient équivalent à a = a + b).

Pour les types qui prennent en charge les deux __iadd__, __add__vous devez donc faire attention à celui que vous utilisez. a += bva appeler __iadd__et muter a, alors que a = a + bva créer un nouvel objet et l'assigner à a. Ce ne sont pas la même opération!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Pour les types immuables (où vous n'avez pas de __iadd__) a += bet a = a + bsont équivalents. C'est ce qui vous permet d'utiliser +=sur des types immuables, ce qui peut sembler une décision de conception étrange jusqu'à ce que vous considériez que sinon vous ne pourriez pas utiliser +=sur des types immuables comme les nombres!

Scott Griffiths
la source
4
Il existe également une __radd__méthode qui peut être appelée parfois (elle est pertinente pour les expressions qui impliquent principalement des sous-classes).
jfs
2
En perspective: + = est utile si la mémoire et la vitesse sont importantes
Norfeldt
3
Sachant que cela étend+= réellement une liste, cela explique pourquoi donne un certain temps revient simplement . x = []; x = x + {}TypeErrorx = []; x += {}[]
zezollo
96

Pour le cas général, voir la réponse de Scott Griffith . Cependant, lorsque vous traitez des listes comme vous, l' +=opérateur est un raccourci pour someListObject.extend(iterableObject). Voir la documentation de extend () .

La extendfonction ajoutera tous les éléments du paramètre à la liste.

Lorsque foo += somethingvous modifiez la liste foosur place, vous ne changez donc pas la référence vers laquelle foopointe le nom , mais vous modifiez directement l'objet de la liste. Avec foo = foo + something, vous créez en fait une nouvelle liste.

Cet exemple de code l'expliquera:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Notez comment la référence change lorsque vous réaffectez la nouvelle liste à l.

Comme barc'est une variable de classe au lieu d'une variable d'instance, la modification sur place affectera toutes les instances de cette classe. Mais lors de la redéfinition self.bar, l'instance aura une variable d'instance distincte self.barsans affecter les autres instances de classe.

AndiDog
la source
7
Ce n'est pas toujours vrai: a = 1; a + = 1; est Python valide, mais ints n'a pas de méthode "extend ()". Vous ne pouvez pas généraliser cela.
e-satis
2
J'ai fait quelques tests, Scott Griffiths a bien compris, donc -1 pour vous.
e-satis
11
@ e-statis: Le PO parlait clairement de listes, et j'ai clairement indiqué que je parlais aussi de listes. Je ne généralise rien.
AndiDog
Supprimé le -1, la réponse est assez bonne. Je pense toujours que la réponse de Griffiths est meilleure.
e-satis
Au début, il est étrange de penser que a += bc'est différent de a = a + bdeux listes aet b. Mais cela a du sens; extendserait plus souvent la chose prévue à faire avec des listes plutôt que de créer une nouvelle copie de la liste entière qui aura une plus grande complexité de temps. Si les développeurs doivent faire attention à ne pas modifier les listes d'origine en place, les tuples sont une meilleure option étant des objets immuables. +=avec des tuples ne peut pas modifier le tuple d'origine.
Pranjal Mittal
22

Le problème ici est qu'il barest défini comme un attribut de classe et non comme une variable d'instance.

Dans foo, l'attribut class est modifié dans la initméthode, c'est pourquoi toutes les instances sont affectées.

Dans foo2, une variable d'instance est définie à l'aide de l'attribut de classe (vide), et chaque instance obtient la sienne bar.

La mise en œuvre "correcte" serait:

class foo:
    def __init__(self, x):
        self.bar = [x]

Bien sûr, les attributs de classe sont tout à fait légaux. En fait, vous pouvez y accéder et les modifier sans créer une instance de la classe comme ceci:

class foo:
    bar = []

foo.bar = [x]
Can Berk Güder
la source
8

Il y a deux choses impliquées ici:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+L'opérateur appelle la __add__méthode sur une liste. Il prend tous les éléments de ses opérandes et crée une nouvelle liste contenant ces éléments en conservant leur ordre.

+=l'opérateur appelle la __iadd__méthode de la liste. Il prend un itérable et ajoute tous les éléments de l'itérable à la liste en place. Il ne crée pas de nouvel objet de liste.

En classe, fool'instruction self.bar += [x]n'est pas une instruction d'affectation mais se traduit en fait par

self.bar.__iadd__([x])  # modifies the class attribute  

qui modifie la liste en place et agit comme la méthode list extend.

En classe foo2, au contraire, l'instruction d'affectation dans la initméthode

self.bar = self.bar + [x]  

peut être déconstruite comme suit:
l'instance n'a pas d'attribut bar(il existe cependant un attribut de classe du même nom), donc elle accède à l'attribut de classe baret crée une nouvelle liste en y ajoutant x. La déclaration se traduit par:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Ensuite, il crée un attribut d'instance baret lui affecte la liste nouvellement créée. Notez que barsur le rhs de l'affectation est différent du barsur le lhs.

Pour les instances de classe foo, barest un attribut de classe et non un attribut d'instance. Par conséquent, toute modification de l'attribut de classe barsera reflétée pour toutes les instances.

Au contraire, chaque instance de la classe foo2a son propre attribut d'instance barqui est différent de l'attribut de classe du même nom bar.

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

J'espère que cela clarifie les choses.

ajay
la source
5

Bien que beaucoup de temps se soit écoulé et que beaucoup de choses correctes aient été dites, il n'y a pas de réponse regroupant les deux effets.

Vous avez 2 effets:

  1. un comportement "spécial", peut-être inaperçu, des listes avec +=(comme indiqué par Scott Griffiths )
  2. le fait que les attributs de classe ainsi que les attributs d'instance sont impliqués (comme indiqué par Can Berk Büder )

En classe foo, la __init__méthode modifie l'attribut de classe. C'est parce que se self.bar += [x]traduit par self.bar = self.bar.__iadd__([x]). __iadd__()est pour la modification sur place, donc il modifie la liste et renvoie une référence à celle-ci.

Notez que l'instance dict est modifiée bien que cela ne soit normalement pas nécessaire car la classe dict contient déjà la même affectation. Donc, ce détail passe presque inaperçu - sauf si vous faites un foo.bar = []après. Ici, les instances barrestent les mêmes grâce à ce fait.

En classe foo2, cependant, la classe barest utilisée, mais pas touchée. Au lieu de cela, un y [x]est ajouté, formant un nouvel objet, comme self.bar.__add__([x])on l'appelle ici, qui ne modifie pas l'objet. Le résultat est ensuite placé dans l'instance dict, donnant à l'instance la nouvelle liste sous forme de dict, tandis que l'attribut de la classe reste modifié.

La distinction entre ... = ... + ...et ... += ...affecte également les affectations par la suite:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Vous pouvez vérifier l'identité des objets avec print id(foo), id(f), id(g)(n'oubliez pas les ()s supplémentaires si vous êtes sur Python3).

BTW: L' +=opérateur est appelé «affectation augmentée» et est généralement destiné à faire des modifications sur place dans la mesure du possible.

glglgl
la source
5

Les autres réponses semblent couvrir à peu près tout cela, bien qu'il semble utile de citer et de se référer aux affectations augmentées PEP 203 :

Ils [les opérateurs d'affectation augmentée] implémentent le même opérateur que leur forme binaire normale, sauf que l'opération est effectuée «en place» lorsque l'objet de gauche le prend en charge, et que le côté gauche n'est évalué qu'une seule fois.

...

L'idée derrière l'affectation augmentée en Python est que ce n'est pas seulement un moyen plus simple d'écrire la pratique courante de stockage du résultat d'une opération binaire dans son opérande de gauche, mais aussi un moyen pour l'opérande de gauche en question de sachez qu'il devrait fonctionner «sur lui-même» plutôt que de créer une copie modifiée de lui-même.

mwardm
la source
1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])
enchevêtrement
la source
0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Nous voyons que lorsque nous essayons de modifier un objet immuable (entier dans ce cas), Python nous donne simplement un objet différent à la place. D'autre part, nous pouvons apporter des modifications à un objet mutable (une liste) et le faire rester le même objet partout.

réf: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Référez-vous également à l'url ci-dessous pour comprendre la copie peu profonde et la copie profonde

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

Roshan ok
la source
# ID est le même pour les listes
roshan ok