python augmented-assignment

python - ¿Por qué+= se comporta inesperadamente en las listas?



augmented-assignment (7)

Aquí hay dos cosas involucradas:

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

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

+= operador llama __iadd__ método __iadd__ 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 la clase foo la declaración self.bar += [x] no es una declaración de asignación, pero 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 la clase foo2 , por el contrario, la instrucción de asignación en el método init

self.bar = self.bar + [x]

puede ser deconstruido como:
La instancia no tiene bar atributos (sin embargo, hay un atributo de clase del mismo nombre) por lo que accede a la bar atributos de clase y crea una nueva lista al agregarle x . La declaración se traduce a:

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

Luego crea una bar atributos de instancia y le asigna la lista recién creada. Tenga en cuenta que la bar en el rhs de la asignación es diferente de la bar en el lhs.

Para instancias de clase foo , la bar es un atributo de clase y no un atributo de instancia. Por lo tanto, cualquier cambio en la bar atributos de clase se reflejará para todas las instancias.

Por el contrario, cada instancia de la clase foo2 tiene su propia bar atributos de instancia que es diferente del atributo de clase de la bar del mismo nombre.

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.

El operador += en Python parece estar funcionando inesperadamente en las 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 cada instancia de la clase, mientras que foo = foo + bar parece comportarse de la manera en que esperaría que las cosas se comportaran.

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


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

Tienes 2 efectos:

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

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

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

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

La distinción entre ... = ... + ... y ... += ... afecta 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 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.


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 el método init , es por eso que todas las instancias se ven afectadas.

En foo2 , una variable de instancia se define utilizando el atributo de clase (vacío) y cada instancia obtiene su propia 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]


La respuesta general es que += intenta llamar al método especial __iadd__ , y si eso no está disponible, intenta usar __add__ en __add__ lugar. Entonces, el problema es con la diferencia entre estos métodos especiales.

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

Entonces, cuando se usa el operador += en un objeto que tiene un __iadd__ definido, el objeto se modifica en su lugar. De lo contrario, intentará usar el __add__ simple 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 ( a += b vuelve equivalente a a = a + b ).

Para los tipos que admiten tanto __iadd__ como __add__ , debe tener cuidado con cuál usar. a += b llamará a __iadd__ y __iadd__ a , mientras que a = a + b creará un nuevo objeto y lo asignará a 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 += b y a = 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.


Las otras respuestas parecerían tenerlo cubierto, aunque parece que vale la pena citar y referirse a las Asignaciones Aumentadas PEP 203 :

Ellos [los operadores de asignación aumentada] implementan el mismo operador como su forma binaria normal, excepto que la operación se realiza "in situ" cuando el objeto del lado izquierdo la 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 manera 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 mano izquierda, sino también una forma para que el operando de la izquierda en cuestión sepa que debería funcionar "en sí mismo", en lugar de crear una copia modificada de sí mismo.


Para el caso general, vea la respuesta de Scott Griffith . Sin embargo, al tratar con listas como usted, el operador += es una abreviatura de someListObject.extend(iterableObject) . Consulte la documentación de extender () .

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

Al hacer foo += something estás modificando la lista foo en su lugar, por lo tanto no cambias la referencia a la que apunta el nombre foo , pero estás 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 bar es una variable de clase en lugar de una variable de instancia, modificar en su lugar afectará todas las instancias de esa clase. Pero al redefinir self.bar , la instancia tendrá una variable de instancia independiente self.bar sin afectar las otras instancias de clase.


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