web-dev-qa-db-fra.com

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

Le += operator in 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 

SORTIE

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

foo += bar semble affecter toutes les instances de la classe, tandis que foo = foo + bar semble se comporter de la façon dont je m'attendrais à ce que les choses se comportent.

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

101
eucalculia

La réponse générale est que += essaie d'appeler le __iadd__ méthode spéciale, et si elle n'est pas disponible, elle essaie d'utiliser __add__ au lieu. Le problème réside donc dans la différence entre ces méthodes spéciales.

Le __iadd__ la méthode spéciale est pour un ajout sur place, c'est-à-dire qu'elle mute l'objet sur lequel elle agit. Le __add__ la méthode spéciale renvoie un nouvel objet et est également utilisée pour la norme + opérateur.

Alors quand le += L'opérateur est utilisé sur un objet qui a un __iadd__ défini l'objet est modifié sur place. Sinon, il essaiera à la place d'utiliser le plain __add__ et retourne un nouvel objet.

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

Pour les types qui prennent en charge à la fois __iadd__ et __add__ vous devez donc faire attention à celui que vous utilisez. a += b appellera __iadd__ et mute a, tandis que a = a + b créera un nouvel objet et l'affectera à a. Ce n'est 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 += b et a = a + b sont équivalents. C'est ce qui vous permet d'utiliser += sur les 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 les types immuables comme les nombres!

110
Scott Griffiths

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

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

Lorsque vous faites foo += something Vous modifiez la liste foo en place, donc vous ne changez pas la référence vers laquelle le nom foo pointe, mais vous changez la liste objet directement. 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 bar est une variable de classe au lieu d'une variable d'instance, la modification en place affectera toutes les instances de cette classe. Mais lors de la redéfinition de self.bar, L'instance aura une variable d'instance distincte self.bar Sans affecter les autres instances de classe.

89
AndiDog

Le problème ici est que bar est défini comme un attribut de classe, pas une variable d'instance.

Dans foo, l'attribut class est modifié dans la méthode init, 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 son propre 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]
22
Can Berk Güder

Bien que beaucoup de temps se soit écoulé et que de nombreuses choses correctes aient été dites, il n'y a pas de réponse qui regroupe 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 )

Dans la classe foo, la méthode __init__ Modifie l'attribut de classe. C'est parce que self.bar += [x] Se traduit par self.bar = self.bar.__iadd__([x]). __iadd__() est destiné à la modification sur place, il modifie donc la liste et y renvoie une référence.

Notez que le dict d'instance est modifié bien que cela ne soit normalement pas nécessaire car le dict de classe contient déjà la même affectation. Donc, ce détail passe presque inaperçu - sauf si vous faites un foo.bar = [] Par la suite. Ici, le bar des instances reste le même grâce à ce fait.

Dans la classe foo2, Cependant, le bar de la classe est utilisé, mais pas touché. Au lieu de cela, un [x] Lui est ajouté, formant un nouvel objet, car self.bar.__add__([x]) est appelé ici, ce qui ne modifie pas l'objet. Le résultat est ensuite placé dans le dict d'instance, donnant à l'instance la nouvelle liste en tant que dict, tandis que l'attribut de la classe reste modifié.

La distinction entre ... = ... + ... Et ... += ... Affecte également les affectations suivantes:

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 () Supplémentaires si vous êtes sur Python3).

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

5
glglgl

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 le __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 maintenant leur ordre.

+= les appels de l'opérateur __iadd__ méthode dans 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 un nouvel objet liste.

Dans la classe foo l'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 de liste extend.

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

self.bar = self.bar + [x]  

peut être déconstruit comme:
L'instance n'a pas d'attribut bar (il existe cependant un attribut de classe du même nom), elle accède donc à l'attribut de classe bar et crée une nouvelle liste en 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 bar et lui assigne la liste nouvellement créée. Notez que bar sur le rhs de l'affectation est différent du bar sur le lhs.

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

Au contraire, chaque instance de la classe foo2 a son propre attribut d'instance bar qui 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.

5
ajay

Les autres réponses semblent avoir à peu près tout couvert, bien qu'il semble intéressant de citer et de faire référence à Augmented Assignments PEP 2 :

Ils [les opérateurs d'affectation augmentés] implémentent le même opérateur que leur forme binaire normale, sauf que l'opération est effectuée "sur place" lorsque le l'objet côté 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 dans Python est que ce n'est pas seulement un moyen plus simple d'écrire la pratique courante de stocker le 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 savoir qu'il doit opérer "sur lui-même", plutôt que de créer une copie modifiée de lui-même.

5
mwardm
>>> 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])
1
tanglei