¿Por qué + = se comporta de forma inesperada en las listas?

118

El +=operador en Python parece estar operando inesperadamente en listas. ¿Alguien puede decirme qué está pasando aquí?

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 

SALIDA

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

foo += barparece afectar a todas las instancias de la clase, mientras que foo = foo + barparece comportarse de la forma en que esperaría que se comporten las cosas.

El +=operador se denomina "operador de asignación compuesta".

eucalculia
fuente
vea la diferencia entre 'extender' y 'agregar' en la lista también
N 1.1
3
No creo que esto muestre algo malo con Python. La mayoría de los lenguajes ni siquiera le permitirían usar el +operador en matrices. Creo que tiene mucho sentido en este caso que +=se agregaría.
Skilldrick
4
Se llama "asignación aumentada", oficialmente.
Martijn Pieters

Respuestas:

138

La respuesta general es que +=intenta llamar al __iadd__método especial y, si no está disponible, intenta usarlo __add__en su lugar. Entonces, el problema es la diferencia entre estos métodos especiales.

El __iadd__método especial es para una adición in situ, es decir, muta el objeto sobre el que actúa. El __add__método especial devuelve un nuevo objeto y también se usa para el +operador estándar .

Entonces, cuando el +=operador se usa en un objeto que tiene una __iadd__definición, el objeto se modifica en su lugar. De lo contrario, intentará usar el plano __add__y devolver un nuevo objeto.

Es por eso que para tipos mutables como listas +=cambia el valor del objeto, mientras que para tipos inmutables como tuplas, cadenas y enteros, se devuelve un nuevo objeto (se a += bvuelve equivalente a a = a + b).

Para los tipos que admiten ambos __iadd__y __add__, por lo tanto, debe tener cuidado con el que usa. a += bllamará __iadd__y mutará a, mientras a = a + bque creará un nuevo objeto y se lo asignará a. ¡No son la misma operación!

>>> 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

Para tipos inmutables (donde no tiene un __iadd__) a += by a = a + bson equivalentes. Esto es lo que le permite usar +=en tipos inmutables, lo que puede parecer una decisión de diseño extraña hasta que considere que, de lo contrario, no podría usar +=en tipos inmutables como números.

Scott Griffiths
fuente
4
También hay un __radd__método que puede ser llamado a veces (es relevante para expresiones que involucran principalmente subclases).
jfs
2
En perspectiva: + = es útil si la memoria y la velocidad son importantes
Norfeldt
3
Sabiendo que en +=realidad se extiende una lista, esto explica por qué x = []; x = x + {}da un TypeErrortiempo x = []; x += {}simplemente regresa [].
zezollo
96

Para el caso general, consulte la respuesta de Scott Griffith . Sin embargo, cuando se trata de listas como usted, el +=operador es una abreviatura de someListObject.extend(iterableObject). Consulte la documentación de extend () .

La extendfunción agregará todos los elementos del parámetro a la lista.

Al hacerlo foo += something, está modificando la lista fooen su lugar, por lo que no cambia la referencia a la que fooapunta el nombre , sino que está cambiando el objeto de la lista directamente. Con foo = foo + something, en realidad estás creando una nueva lista.

Este código de ejemplo lo explicará:

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

Observe cómo cambia la referencia cuando reasigna la nueva lista a l.

Como bares una variable de clase en lugar de una variable de instancia, la modificación en el lugar afectará a todas las instancias de esa clase. Pero al redefinir self.bar, la instancia tendrá una variable de instancia separada self.barsin afectar las otras instancias de clase.

AndiDog
fuente
7
Esto no siempre es cierto: a = 1; a + = 1; es Python válido, pero los ints no tienen ningún método "extend ()". No puedes generalizar esto.
e-satis
2
Hice algunas pruebas, Scott Griffiths lo hizo bien, así que -1 para ti.
e-satis
11
@ e-statis: El OP estaba hablando claramente de listas, y yo dije claramente que también estoy hablando de listas. No estoy generalizando nada.
AndiDog
Eliminado el -1, la respuesta es suficiente. Sin embargo, sigo pensando que la respuesta de Griffiths es mejor.
e-satis
Al principio se siente extraño pensar que a += bes diferente de a = a + bdos listas ay b. Pero tiene sentido; extendsería más a menudo lo que se pretendía hacer con las listas en lugar de crear una nueva copia de la lista completa que tendrá una mayor complejidad de tiempo. Si los desarrolladores deben tener cuidado de no modificar las listas originales en su lugar, las tuplas son una mejor opción al ser objetos inmutables. +=con tuplas no puede modificar la tupla original.
Pranjal Mittal
22

El problema aquí es que barse define como un atributo de clase, no como una variable de instancia.

En foo, el atributo de clase se modifica en el initmétodo, por eso todas las instancias se ven afectadas.

En foo2, una variable de instancia se define mediante el atributo de clase (vacío), y cada instancia obtiene la suya bar.

La implementación "correcta" sería:

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

Por supuesto, los atributos de clase son completamente legales. De hecho, puede acceder y modificarlos sin crear una instancia de la clase como esta:

class foo:
    bar = []

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

Hay dos cosas involucradas aquí:

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

+el operador llama al __add__método en una lista. Toma todos los elementos de sus operandos y crea una nueva lista que contiene esos elementos manteniendo su orden.

+=el operador llama al __iadd__método en la lista. Toma un iterable y agrega todos los elementos del iterable a la lista en su lugar. No crea un nuevo objeto de lista.

En clase, foola declaración self.bar += [x]no es una declaración de asignación, sino que en realidad se traduce en

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

que modifica la lista en su lugar y actúa como el método de lista extend.

En clase foo2, por el contrario, la declaración de asignación en el initmétodo

self.bar = self.bar + [x]  

se puede deconstruir como:
La instancia no tiene atributo bar(aunque hay un atributo de clase con el mismo nombre) por lo que accede al atributo de clase bary crea una nueva lista añadiéndola x. La declaración se traduce en:

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

Luego crea un atributo de instancia bary le asigna la lista recién creada. Tenga baren cuenta que en el lado derecho de la asignación es diferente del lado derecho bar.

Para instancias de clase foo, bares un atributo de clase y no un atributo de instancia. Por lo tanto, cualquier cambio en el atributo de clase barse reflejará en todas las instancias.

Por el contrario, cada instancia de la clase foo2tiene su propio atributo de instancia barque es diferente del atributo de clase del mismo nombre bar.

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

Espero que esto aclare las cosas.

ajay
fuente
5

Aunque ha pasado mucho tiempo y se han dicho muchas cosas correctas, no hay una respuesta que agrupe ambos efectos.

Tienes 2 efectos:

  1. un comportamiento "especial", tal vez inadvertido, de listas con += (como afirma Scott Griffiths )
  2. el hecho de que los atributos de clase, así como los atributos de instancia, están involucrados (como lo indica Can Berk Büder )

En clase foo, el __init__método modifica el atributo de clase. Es porqueself.bar += [x] traduce en self.bar = self.bar.__iadd__([x]). __iadd__()es para modificación in situ, por lo que modifica la lista y devuelve una referencia a ella.

Tenga en cuenta que el dictado de instancia se modifica, aunque normalmente no sería necesario ya que el dictado de clase ya contiene la misma asignación. Así que este detalle pasa casi desapercibido, excepto si haces unfoo.bar = [] después. Aquí las instancias se barmantienen iguales gracias a dicho hecho.

En clase foo2, sin embargo, barse usa el de la clase , pero no se toca. En cambio, un[x] se le agrega a, formando un nuevo objeto, como self.bar.__add__([x])se llama aquí, que no modifica el objeto. El resultado se coloca en el dict de la instancia, dando a la instancia la nueva lista como un dict, mientras que el atributo de la clase permanece modificado.

La distinción entre ... = ... + ...y ... += ...afecta también a las asignaciones posteriores:

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 

Puede verificar la identidad de los objetos con print id(foo), id(f), id(g) (no olvide los ()correos electrónicos adicionales si está en Python3).

Por cierto: el +=operador se denomina "asignación aumentada" y, en general, está destinado a realizar modificaciones in situ en la medida de lo posible.

glglgl
fuente
5

Las otras respuestas parecen tenerlo cubierto, aunque vale la pena citar y referirse a las asignaciones aumentadas PEP 203 :

Ellos [los operadores de asignación aumentada] implementan el mismo operador que su forma binaria normal, excepto que la operación se realiza "in situ" cuando el objeto del lado izquierdo lo admite, y que el lado izquierdo solo se evalúa una vez.

...

La idea detrás de la asignación aumentada en Python es que no es solo una forma más fácil de escribir la práctica común de almacenar el resultado de una operación binaria en su operando de la izquierda, sino también una forma para que el operando de la izquierda en cuestión Sepa que debería operar "sobre sí mismo", en lugar de crear una copia modificada de sí mismo.

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

Vemos que cuando intentamos modificar un objeto inmutable (entero en este caso), Python simplemente nos da un objeto diferente. Por otro lado, podemos realizar cambios en un objeto mutable (una lista) y hacer que siga siendo el mismo objeto en todo momento.

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

También consulte la URL a continuación para comprender la copia superficial y la copia profunda

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

roshan ok
fuente
# ID es el mismo para Listas
Roshan ok