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é".
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!
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.
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]
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:
+=
(comme indiqué par Scott Griffiths )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.
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.
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.
>>> 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])