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 += bar
parece afectar a todas las instancias de la clase, mientras que foo = foo + bar
parece comportarse de la forma en que esperaría que se comporten las cosas.
El +=
operador se denomina "operador de asignación compuesta".
python
augmented-assignment
eucalculia
fuente
fuente
+
operador en matrices. Creo que tiene mucho sentido en este caso que+=
se agregaría.Respuestas:
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 (sea += b
vuelve equivalente aa = a + b
).Para los tipos que admiten ambos
__iadd__
y__add__
, por lo tanto, debe tener cuidado con el que usa.a += b
llamará__iadd__
y mutaráa
, mientrasa = a + b
que creará un nuevo objeto y se lo asignaráa
. ¡No son la misma operación!Para tipos inmutables (donde no tiene un
__iadd__
)a += b
ya = a + b
son 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.fuente
__radd__
método que puede ser llamado a veces (es relevante para expresiones que involucran principalmente subclases).+=
realidad se extiende una lista, esto explica por quéx = []; x = x + {}
da unTypeError
tiempox = []; x += {}
simplemente regresa[]
.Para el caso general, consulte la respuesta de Scott Griffith . Sin embargo, cuando se trata de listas como usted, el
+=
operador es una abreviatura desomeListObject.extend(iterableObject)
. Consulte la documentación de extend () .La
extend
función agregará todos los elementos del parámetro a la lista.Al hacerlo
foo += something
, está modificando la listafoo
en su lugar, por lo que no cambia la referencia a la quefoo
apunta el nombre , sino que está cambiando el objeto de la lista directamente. Confoo = foo + something
, en realidad estás creando una nueva lista.Este código de ejemplo lo explicará:
Observe cómo cambia la referencia cuando reasigna la nueva lista a
l
.Como
bar
es 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 redefinirself.bar
, la instancia tendrá una variable de instancia separadaself.bar
sin afectar las otras instancias de clase.fuente
a += b
es diferente dea = a + b
dos listasa
yb
. Pero tiene sentido;extend
serí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.El problema aquí es que
bar
se define como un atributo de clase, no como una variable de instancia.En
foo
, el atributo de clase se modifica en elinit
mé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 suyabar
.La implementación "correcta" sería:
Por supuesto, los atributos de clase son completamente legales. De hecho, puede acceder y modificarlos sin crear una instancia de la clase como esta:
fuente
Hay dos cosas involucradas aquí:
+
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,
foo
la declaraciónself.bar += [x]
no es una declaración de asignación, sino que en realidad se traduce enque 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 elinit
métodose 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 clasebar
y crea una nueva lista añadiéndolax
. La declaración se traduce en:Luego crea un atributo de instancia
bar
y le asigna la lista recién creada. Tengabar
en cuenta que en el lado derecho de la asignación es diferente del lado derechobar
.Para instancias de clase
foo
,bar
es un atributo de clase y no un atributo de instancia. Por lo tanto, cualquier cambio en el atributo de clasebar
se reflejará en todas las instancias.Por el contrario, cada instancia de la clase
foo2
tiene su propio atributo de instanciabar
que es diferente del atributo de clase del mismo nombrebar
.Espero que esto aclare las cosas.
fuente
Aunque ha pasado mucho tiempo y se han dicho muchas cosas correctas, no hay una respuesta que agrupe ambos efectos.
Tienes 2 efectos:
+=
(como afirma Scott Griffiths )En clase
foo
, el__init__
método modifica el atributo de clase. Es porqueself.bar += [x]
traduce enself.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 un
foo.bar = []
después. Aquí las instancias sebar
mantienen iguales gracias a dicho hecho.En clase
foo2
, sin embargo,bar
se usa el de la clase , pero no se toca. En cambio, un[x]
se le agrega a, formando un nuevo objeto, comoself.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: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.fuente
Las otras respuestas parecen tenerlo cubierto, aunque vale la pena citar y referirse a las asignaciones aumentadas PEP 203 :
...
fuente
fuente
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/
fuente