¿Por qué funciona b + = (4,) y b = b + (4,) no funciona cuando b es una lista?

77

Si tomamos b = [1,2,3]y tratamos de hacer:b+=(4,)

Regresa b = [1,2,3,4], pero si intentamos hacerlo b = b + (4,)no funciona.

b = [1,2,3]
b+=(4,) # Prints out b = [1,2,3,4]
b = b + (4,) # Gives an error saying you can't add tuples and lists

Esperaba b+=(4,)fallar ya que no puedes agregar una lista y una tupla, pero funcionó. Así que intenté b = b + (4,)esperar obtener el mismo resultado, pero no funcionó.

Supun Dasantha Kuruppu
fuente
44
Creo que se puede encontrar una respuesta aquí .
jochen
Al principio leí mal esto e intenté cerrarlo como demasiado ancho, luego lo retracté. Luego pensé que tenía que ser un duplicado, pero no solo no podía volver a emitir un voto, sino que me arranqué el pelo tratando de encontrar otras respuestas como esas. : /
Karl Knechtel
Pregunta muy similar: stackoverflow.com/questions/58048664/…
sanyash

Respuestas:

70

El problema con las preguntas de "por qué" es que generalmente pueden significar varias cosas diferentes. Trataré de responder a cada una que creo que tengas en mente.

"¿Por qué es posible que funcione de manera diferente?" lo cual es respondido por ej . esto . Básicamente, +=trata de usar diferentes métodos del objeto: __iadd__(que solo está marcado en el lado izquierdo), vs __add__y __radd__("suma inversa", marcado en el lado derecho si el lado izquierdo no tiene __add__) para +.

"¿Qué hace exactamente cada versión?" En resumen, el list.__iadd__método hace lo mismo que list.extend(pero debido al diseño del lenguaje, todavía hay una asignación de vuelta).

Esto también significa, por ejemplo, que

>>> a = [1,2,3]
>>> b = a
>>> a += [4] # uses the .extend logic, so it is still the same object
>>> b # therefore a and b are still the same list, and b has the `4` added
[1, 2, 3, 4]
>>> b = b + [5] # makes a new list and assigns back to b
>>> a # so now a is a separate list and does not have the `5`
[1, 2, 3, 4]

+, por supuesto, crea un nuevo objeto, pero explícitamente requiere otra lista en lugar de intentar extraer elementos de una secuencia diferente.

"¿Por qué es útil que + = haga esto? Es más eficiente; el extendmétodo no tiene que crear un nuevo objeto. Por supuesto, esto tiene algunos efectos sorprendentes a veces (como arriba), y en general Python no se trata realmente de eficiencia , pero estas decisiones se tomaron hace mucho tiempo.

"¿Cuál es la razón para no permitir agregar listas y tuplas con +?" Ver aquí (gracias, @ splash58); Una idea es que (tupla + lista) debería producir el mismo tipo que (lista + tupla), y no está claro de qué tipo debería ser el resultado. +=no tiene este problema, porque a += bobviamente no debería cambiar el tipo de a.

Karl Knechtel
fuente
2
Oof, muy bien. Y las listas no usan |, así que eso arruina un poco mi ejemplo. Si más tarde pienso en un ejemplo más claro, lo cambiaré.
Karl Knechtel
1
Por cierto, |para conjuntos es un operador de conmutación, pero +para las listas no lo es. Por esa razón, no creo que el argumento sobre la ambigüedad de tipo sea particularmente fuerte. Dado que el operador no conmuta, ¿por qué requerir lo mismo para los tipos? Uno podría estar de acuerdo en que el resultado tiene el tipo de lhs. Por otro lado, al restringir list + iterator, se alienta al desarrollador a ser más explícito sobre su intención. Si desea crear una nueva lista que contiene el material de aextenderse por las cosas de bque ya hay una manera de hacer esto: new = a.copy(); new += b. Es una línea más pero cristalina.
a_guest
La razón por la cual se a += bcomporta de manera diferente que a = a + bno es la eficiencia. En realidad, Guido consideró el comportamiento elegido menos confuso. Imagine una función que recibe una lista acomo argumento y luego lo hace a += [1, 2, 3]. Esta sintaxis ciertamente parece que está modificando la lista en su lugar, en lugar de crear una nueva lista, por lo que se tomó la decisión de que debería comportarse de acuerdo con la intuición de la mayoría de las personas sobre el resultado esperado. Sin embargo, el mecanismo también tuvo que funcionar para tipos inmutables como ints, lo que condujo al diseño actual.
Sven Marnach
Personalmente, creo que el diseño es en realidad más confuso que simplemente hacer a += buna taquigrafía a = a + b, como lo hizo Ruby, pero puedo entender cómo llegamos allí.
Sven Marnach
21

No son equivalentes:

b += (4,)

es la abreviatura de:

b.extend((4,))

mientras +concatena listas, entonces por:

b = b + (4,)

intentas concatenar una tupla en una lista

alfasin
fuente
14

Cuando haces esto:

b += (4,)

se convierte a esto:

b.__iadd__((4,)) 

Debajo del capó que llama b.extend((4,)), extendacepta un iterador y por eso también funciona:

b = [1,2,3]
b += range(2)  # prints [1, 2, 3, 0, 1]

pero cuando haces esto:

b = b + (4,)

se convierte a esto:

b = b.__add__((4,)) 

aceptar solo objeto de lista.

Charif DZ
fuente
4

De los documentos oficiales, para los tipos de secuencia mutable, ambos:

s += t
s.extend(t)

se definen como:

se extiende scon el contenido det

Lo cual es diferente de ser definido como:

s = s + t    # not equivalent in Python!

Esto también significa que cualquier tipo de secuencia funcionarát , incluida una tupla como en su ejemplo.

¡Pero también funciona para rangos y generadores! Por ejemplo, también puedes hacer:

s += range(3)
Bellota
fuente
3

Los operadores de asignación "aumentada" como +=se introdujeron en Python 2.0, que se lanzó en octubre de 2000. El diseño y la justificación se describen en PEP 203 . Uno de los objetivos declarados de estos operadores era el apoyo de las operaciones en el lugar. Escritura

a = [1, 2, 3]
a += [4, 5, 6]

se supone que actualiza la lista a en su lugar . Esto es importante si hay otras referencias a la lista a, por ejemplo, cuándo ase recibió como argumento de función.

Sin embargo, la operación no siempre puede ocurrir en el lugar, ya que muchos tipos de Python, incluidos los enteros y las cadenas, son inmutables , por lo que, por ejemplo, i += 1un entero ino puede funcionar en su lugar.

En resumen, se suponía que los operadores de asignación aumentada trabajarían en su lugar cuando fuera posible y, de lo contrario, crearían un nuevo objeto. Para facilitar estos objetivos de diseño, x += yse especificó que la expresión se comportara de la siguiente manera:

  • Si x.__iadd__se define, x.__iadd__(y)se evalúa.
  • De lo contrario, si x.__add__se implementa x.__add__(y)se evalúa.
  • De lo contrario, si y.__radd__se implementa y.__radd__(x)se evalúa.
  • De lo contrario, generar un error.

El primer resultado obtenido por este proceso se asignará nuevamente a x(a menos que ese resultado sea el NotImplementedsingleton, en cuyo caso la búsqueda continúa con el siguiente paso).

Este proceso permite la implementación de tipos que admiten la modificación en el lugar __iadd__(). Los tipos que no admiten la modificación en el lugar no necesitan agregar ningún método mágico nuevo, ya que Python recurrirá automáticamente a esencialmente x = x + y.

Así que finalmente lleguemos a su pregunta real: por qué puede agregar una tupla a una lista con un operador de asignación aumentada. De memoria, la historia de esto fue más o menos así: el list.__iadd__()método se implementó simplemente para llamar al list.extend()método ya existente en Python 2.0. Cuando se introdujeron los iteradores en Python 2.1, el list.extend()método se actualizó para aceptar iteradores arbitrarios. El resultado final de estos cambios fue que my_list += my_tuplefuncionó a partir de Python 2.1. El list.__add__()método, sin embargo, nunca se debe apoyar iteradores arbitrarias como el argumento de la derecha - esto fue considerado inapropiado para un lenguaje fuertemente tipado.

Personalmente, creo que la implementación de operadores aumentados terminó siendo demasiado compleja en Python. Tiene muchos efectos secundarios sorprendentes, por ejemplo, este código:

t = ([42], [43])
t[0] += [44]

La segunda línea se eleva TypeError: 'tuple' object does not support item assignment, pero la operación se realiza con éxito de todos modos , tserá ([42, 44], [43])después de ejecutar la línea que genera el error.

Sven Marnach
fuente
¡Bravo! Tener una referencia a la PEP es especialmente útil. Agregué un enlace en el otro extremo, a una pregunta SO anterior sobre el comportamiento de lista en tupla. Cuando miro hacia atrás a cómo era Python antes de 2.3 más o menos, parece prácticamente inutilizable en comparación con hoy ... (y todavía tengo un vago recuerdo de intentar y no lograr que 1.5 haga algo útil en una Mac muy antigua)
Karl Knechtel
2

La mayoría de la gente esperaría que X + = Y sea equivalente a X = X + Y. De hecho, la Referencia de bolsillo de Python (4ª edición) de Mark Lutz dice en la página 57 "Los siguientes dos formatos son más o menos equivalentes: X = X + Y, X + = Y ". Sin embargo, las personas que especificaron Python no los hicieron equivalentes. Posiblemente fue un error que resultará en horas de tiempo de depuración por parte de programadores frustrados durante el tiempo que Python permanezca en uso, pero ahora es como Python lo es. Si X es un tipo de secuencia mutable, X + = Y es equivalente a X.extend (Y) y no a X = X + Y.

zizzler
fuente
> Posiblemente fue un error que resultará en horas de tiempo de depuración por parte de programadores frustrados mientras Python permanezca en uso <- ¿realmente sufrió por esto? Pareces estar hablando por experiencia. Me gustaría mucho escuchar tu historia.
Veky
1

Como se explica aquí , si arrayno implementa el __iadd__método, b+=(4,)sería una falta de personal, b = b + (4,)pero obviamente no lo es, también lo arrayhace el __iadd__método de implementación . Aparentemente, la implementación del __iadd__método es algo como esto:

def __iadd__(self, x):
    self.extend(x)

Sin embargo, sabemos que el código anterior no es la implementación real del __iadd__método, pero podemos suponer y aceptar que hay algo parecido al extendmétodo, que acepta tuppleentradas.

Hamidreza
fuente