¿Cuándo es “i += x” diferente de “i = i + x” en Python?

6 minutos de lectura

¿Cuándo es "i += x" diferente de "i = i + x" en Python?
MarJamRob

me dijeron que += puede tener diferentes efectos que la notación estándar de i = i +. ¿Hay algún caso en el que i += 1 sería diferente de i = i + 1?

  • += actúa como extend() en caso de listas.

    –Ashwini Chaudhary

    13 de marzo de 2013 a las 3:38


  • @AshwiniChaudhary Esa es una distinción bastante sutil, considerando que i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6] es True. Es posible que muchos desarrolladores no se den cuenta de que id(i) cambia para una operación, pero no para la otra.

    – kojiro

    13 de marzo de 2013 a las 12:22

  • @kojiro: si bien es una distinción sutil, creo que es importante.

    – mgilson

    13 de marzo de 2013 a las 16:44

  • @mgilson es importante, por lo que sentí que necesitaba una explicación. 🙂

    – kojiro

    13 de marzo de 2013 a las 18:55

  • Pregunta relacionada con las diferencias entre los dos en Java: stackoverflow.com/a/7456548/245966

    – jakub.g

    19 de marzo de 2013 a las 19:38

¿Cuándo es "i += x" diferente de "i = i + x" en Python?
mgilson

Esto depende completamente del objeto. i.

+= llama al __iadd__ método (si existe, recurriendo a __add__ si no existe) mientras que + llama al __add__ método1 o la __radd__ método en algunos casos2.

Desde la perspectiva de la API, __iadd__ se supone que se usa para modificar objetos mutables en su lugar (devolviendo el objeto que fue mutado) mientras que __add__ debería devolver un nueva instancia de algo. Para inmutable objetos, ambos métodos devuelven una nueva instancia, pero __iadd__ colocará la nueva instancia en el espacio de nombres actual con el mismo nombre que tenía la instancia anterior. Esta es la razón por

i = 1
i += 1

parece incrementar i. En realidad, obtienes un nuevo entero y lo asignas “encima de” i — perdiendo una referencia al entero antiguo. En este caso, i += 1 es exactamente igual que i = i + 1. Pero, con la mayoría de los objetos mutables, es una historia diferente:

Como ejemplo concreto:

a = [1, 2, 3]
b = a
b += [1, 2, 3]
print a  #[1, 2, 3, 1, 2, 3]
print b  #[1, 2, 3, 1, 2, 3]

comparado con:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

fíjate cómo en el primer ejemplo, ya que b y a referencia al mismo objeto, cuando uso += en ben realidad cambia b (y a también ve ese cambio; después de todo, hace referencia a la misma lista). Sin embargo, en el segundo caso, cuando hago b = b + [1, 2, 3]esto toma la lista que b está haciendo referencia y lo concatena con una nueva lista [1, 2, 3]. Luego almacena la lista concatenada en el espacio de nombres actual como b — Sin tener en cuenta lo que b era la línea antes.


1en la expresión x + ysi x.__add__ no está implementado o si x.__add__(y) devoluciones NotImplemented y x y y tienen diferentes tiposluego x + y trata de llamar y.__radd__(x). Por lo tanto, en el caso de que tenga

foo_instance += bar_instance

si Foo no implementa __add__ o __iadd__ entonces el resultado aquí es el mismo que

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2en la expresión foo_instance + bar_instance, bar_instance.__radd__ será juzgado antes foo_instance.__add__ si el tipo de bar_instance es una subclase del tipo de foo_instance (p.ej issubclass(Bar, Foo)). La razón de esto es que Bar es en cierto sentido un objeto de “nivel superior” que Foo entonces Bar debería obtener la opción de anular Foocomportamiento de

  • Bien, += llamadas __iadd__ si existiera, y recurre a agregar y volver a vincular de lo contrario. Es por eso i = 1; i += 1 funciona aunque no hay int.__iadd__. Pero aparte de esa liendre menor, excelentes explicaciones.

    – abarnert

    13 de marzo de 2013 a las 3:31


  • @abarnert: siempre supuse que int.__iadd__ acabo de llamar __add__. Me alegro de haber aprendido algo nuevo hoy :).

    – mgilson

    13 de marzo de 2013 a las 3:34


  • @abarnert – Supongo que tal vez sea completo, x + y llamadas y.__radd__(x) si x.__add__ no existe (o devuelve NotImplemented y x y y son de diferentes tipos)

    – mgilson

    13 de marzo de 2013 a las 13:59

  • Si realmente quiere ser completista, debe mencionar que el bit “si existe” pasa por los mecanismos habituales de getattr, excepto por algunas peculiaridades con las clases clásicas, y para los tipos implementados en la API de C, en su lugar busca cualquiera nb_inplace_add o sq_inplace_concat, y esas funciones de la API de C tienen requisitos más estrictos que los métodos dunder de Python, y… Pero no creo que eso sea relevante para la respuesta. La distinción principal es que += intenta hacer un agregado en el lugar antes de volver a actuar como +que creo que ya has explicado.

    – abarnert

    13 de marzo de 2013 a las 18:51

  • Sí, supongo que tienes razón… Aunque podría volver a la postura de que la API de C no es parte de pitón. es parte de Cpython :-PAGS

    – mgilson

    13 de marzo de 2013 a las 19:49

¿Cuándo es "i += x" diferente de "i = i + x" en Python?
abarnert

Debajo de las sábanas, i += 1 hace algo como esto:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

Tiempo i = i + 1 hace algo como esto:

i = i.__add__(1)

Esta es una ligera simplificación excesiva, pero entiendes la idea: Python le da a los tipos una forma de manejar += especialmente, mediante la creación de un __iadd__ método, así como un __add__.

La intención es que los tipos mutables, como listse mutarán en __iadd__ (y luego volver selfa menos que esté haciendo algo muy complicado), mientras que los tipos inmutables, como intsimplemente no lo implementará.

Por ejemplo:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

Porque l2 es el mismo objeto que l1y tu mutaste l1también mutaste l2.

Pero:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

Toma, no mutaste l1; en su lugar, creó una nueva lista, l1 + [3]y rebotar el nombre l1 señalarlo, dejando l2 apuntando a la lista original.

(En el += versión, también estabas reenlazando l1es que en ese caso lo estabas revinculando al mismo list ya estaba vinculado, por lo que generalmente puede ignorar esa parte).

  • lo hace __iadd__ en realidad llamar __add__ en el caso de un AttributeError?

    – mgilson

    13 de marzo de 2013 a las 3:31

  • Bien, i.__iadd__ no llama __add__; es i += 1 que llama __add__.

    – abarnert

    13 de marzo de 2013 a las 3:32

  • errr… Sí, eso es lo que quise decir. Interesante. No me di cuenta de que se hizo automáticamente.

    – mgilson

    13 de marzo de 2013 a las 3:33


  • El primer intento es en realidad i = i.__iadd__(1)iadd lata modificar el objeto en su lugar, pero no es necesario, por lo que se espera que devuelva el resultado en cualquier caso.

    – lvc

    13 de marzo de 2013 a las 3:33

  • Tenga en cuenta que esto significa que operator.iadd llamadas __add__ en AttributeErrorpero no puede volver a vincular el resultado… así que i=1; operator.iadd(i, 1) regresa 2 y se va i ajustado a 1. Lo cual es un poco confuso.

    – abarnert

    13 de marzo de 2013 a las 3:34

¿Cuándo es "i += x" diferente de "i = i + x" en Python?
Deqing

Aquí hay un ejemplo que compara directamente i += x con i = i + x:

def foo(x):
  x = x + [42]

def bar(x):
  x += [42]

c = [27]
foo(c); # c is not changed
bar(c); # c is changed to [27, 42]

¿Ha sido útil esta solución?