178 votes

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

Mon ami m'a envoyé le code Python suivant pour démontrer ce qu'elle pense est le problème avec Python. Pour le moment, je suis d'accord avec elle. Tous les programmeurs expérimentés, j'ai montré que ce sont tout aussi confus. Quelqu'un peut me dire ce qui se passe?

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 à chaque instance de la classe, alors que foo = foo + bar semble se comporter de la façon dont je voudrais que les choses se comporter.

J'ai essayé de googler, mais je ne suis pas sûr de ce que l' += opérateur est appelé officiellement, donc, n'ont pas été en mesure de trouver quoi que ce soit.

204voto

Scott Griffiths Points 8867

La réponse générale est qu' += tente d'appeler l' __iadd__ méthode spéciale, et si ce n'est pas disponible, il essaie d'utiliser __add__ à la place. Donc la question est la différence entre ces méthodes.

L' __iadd__ méthode particulière est un endroit plus, qu'en est-il de mutation de l'objet qu'il s'agit. L' __add__ spécial méthode renvoie un nouvel objet et est également utilisé pour la norme + de l'opérateur.

Ainsi, lorsque l' += opérateur est utilisé sur un objet qui a un __iadd__ défini l'objet est modifié en place. Sinon, il va plutôt essayer d'utiliser la plaine __add__ et renvoyer un nouvel objet.

C'est pourquoi, pour mutable types comme des listes += des modifications de l'objet de valeur, alors que pour immuable des types comme tuples, chaînes et les nombres entiers d'un nouvel objet qui est retourné à la place (a += b devient l'équivalent a = a + b).

Pour les types qui prennent en charge les deux __iadd__ et __add__ , vous devez donc être prudent lequel vous utilisez. a += b appellerons __iadd__ et muter a, alors que a = a + b permettra de créer un nouvel objet et de l'attribuer à l' a. Ils 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 immuable types (où vous n'avez pas un __iadd__) a += b et a = a + b sont équivalentes. C'est ce qui vous permet d'utiliser += sur immuable types, ce qui peut sembler une étrange conception de la décision jusqu'à ce que vous considérez que sinon, vous ne pouviez pas utiliser += sur immuable types tels que des chiffres!

110voto

AndiDog Points 28417

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

L' extend fonction aura pour effet d'ajouter tous les éléments du paramètre à la liste.

Lorsque vous effectuez foo += something vous êtes à la modification de la liste foo , ce que vous ne modifiez pas la référence que le nom de l' foo de points à, mais vous êtes à la modification de la liste de l'objet directement. Avec foo = foo + something, vous êtes en fait la création d'une nouvelle liste.

Cet exemple de code va l'expliquer:

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

Notez comment les changements de référence lorsque vous réaffectez la nouvelle liste d' l.

En tant que bar est une variable de classe au lieu d'une variable d'instance, en modifiant en place affecte toutes les instances de cette classe. Mais quand la redéfinition self.bar, l'instance disposerez d'une variable d'instance, self.bar sans affecter les autres instances de classe.

22voto

Can Berk Güder Points 39887

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

En foo, l'attribut de classe est modifiée dans l' init méthode, c'est pourquoi toutes les instances sont concernées.

En foo2, une variable d'instance est définie à l'aide de l' (vide) de l'attribut de classe, et chaque instance possède son propre bar.

La "bonne" mise en œuvre serait:

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

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

class foo:
    bar = []

foo.bar = [x]

7voto

glglgl Points 35668

Bien que beaucoup de temps a passé et beaucoup de choses correctes ont été dit, il n'y a pas de réponse qui bundles ces deux effets.

Vous avez 2 effets:

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

Dans la classe foo, __init__ méthode permet de modifier l'attribut de classe. C'est parce qu' self.bar += [x] se traduit par self.bar = self.bar.__iadd__([x]). __iadd__() est pour en place les modifications, donc il modifie la liste et retourne une référence à elle.

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

Dans la classe foo2, cependant, la classe bar est utilisé, mais pas touché. Au lieu de cela, un [x] est ajouté, formant un nouvel objet, comme self.bar.__add__([x]) qui est appelé ici, ce qui ne veut pas modifier l'objet. Le résultat est mis dans l'instance dict ensuite, en donnant l'exemple de la nouvelle liste des dict, tandis que la classe de l'attribut reste modifiés.

La distinction entre ... = ... + ... et ... += ... affecte aussi bien les missions 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 le vérifier l'identité des objets avec print id(foo), id(f), id(g) (n'oubliez pas le supplémentaires ()s si vous êtes sur Python3).

BTW: L' += opérateur est appelé "augmentée" d'assignation et d'une façon générale, vise à faire en place des modifications aussi loin que possible.

1voto

tanglei Points 141
>>> 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])

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X