Me acabo de dar cuenta de que en Python, si uno escribe
for i in a:
i += 1
Los elementos de la lista original en a
realidad no se verán afectados en absoluto, ya que la variable i
resulta ser solo una copia del elemento original en a
.
Para modificar el elemento original,
for index, i in enumerate(a):
a[index] += 1
sería necesario.
Realmente me sorprendió este comportamiento. Esto parece ser muy contradictorio, aparentemente diferente de otros idiomas y ha resultado en errores en mi código que tuve que depurar durante mucho tiempo hoy.
He leído Python Tutorial antes. Solo para estar seguro, revisé el libro nuevamente en este momento, y ni siquiera menciona este comportamiento en absoluto.
¿Cuál es el razonamiento detrás de este diseño? ¿Se espera que sea una práctica estándar en muchos idiomas para que el tutorial crea que los lectores deberían entenderlo de forma natural? ¿En qué otros idiomas está presente el mismo comportamiento en la iteración, al que debería prestar atención en el futuro?
i
es inmutable o si está llevando a cabo una operación no mutante. Con una lista anidadafor i in a: a.append(1)
tendría un comportamiento diferente; Python no copia las listas anidadas. Sin embargo, los enteros son inmutables y la suma devuelve un nuevo objeto, no cambia el anterior.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. Lo mismo en C #i = i + 1
afectara
?Respuestas:
Ya respondí una pregunta similar últimamente y es muy importante darse cuenta de que
+=
puede tener diferentes significados:Si el tipo de datos implementa la suma in situ (es decir, tiene una
__iadd__
función que funciona correctamente ), los datos a los que sei
refiere se actualizan (no importa si están en una lista o en otro lugar).Si el tipo de datos no implementa un
__iadd__
método, lai += x
declaración es solo azúcar sintácticai = i + x
, por lo que se crea un nuevo valor y se asigna al nombre de la variablei
.Si el tipo de datos se implementa
__iadd__
pero hace algo extraño. Podría ser posible que esté actualizado ... o no, eso depende de lo que se implemente allí.Los enteros, flotadores y cadenas de Python no se implementan,
__iadd__
por lo que no se actualizarán in situ. Sin embargo, otros tipos de datos comonumpy.array
olist
s lo implementan y se comportarán como esperaba. Por lo tanto, no se trata de copiar o no copiar al iterar (normalmente no hace copias paralist
sytuple
s, ¡pero eso también depende de la implementación de los contenedores__iter__
y el__getitem__
método!), Sino más bien del tipo de datos has almacenado en tua
.fuente
Aclaración - terminología
Python no distingue entre los conceptos de referencia y puntero . Por lo general, solo usan el término referencia , pero si compara con lenguajes como C ++ que tienen esa distinción, está mucho más cerca de un puntero .
Dado que el autor de la pregunta claramente proviene de antecedentes de C ++, y dado que esa distinción, que se requiere para la explicación, no existe en Python, he elegido usar la terminología de C ++, que es:
void foo(int x);
es una firma de una función que recibe un entero por valor .void foo(int* x);
es una firma de una función que recibe un entero por puntero .void foo(int& x);
es una firma de una función que recibe un número entero por referencia .¿Qué quiere decir "diferente de otros idiomas"? La mayoría de los idiomas que conozco que admiten bucles para cada uno están copiando el elemento a menos que se indique específicamente lo contrario.
Específicamente para Python (aunque muchas de estas razones pueden aplicarse a otros lenguajes con conceptos arquitectónicos o filosóficos similares):
Este comportamiento puede causar errores para las personas que no lo conocen, pero el comportamiento alternativo puede causar errores incluso para aquellos que lo conocen . Cuando asigna una variable (
i
), por lo general, no se detiene y considera todas las otras variables que se cambiarían debido a ella (a
). Limitar el alcance en el que está trabajando es un factor importante para prevenir el código de espagueti y, por lo tanto, la iteración por copia suele ser el valor predeterminado incluso en los idiomas que admiten la iteración por referencia.Las variables de Python siempre son un puntero único, por lo que es barato iterar por copia, más barato que iterar por referencia, lo que requeriría un aplazamiento adicional cada vez que acceda al valor.
Python no tiene el concepto de variables de referencia como, por ejemplo, C ++. Es decir, todas las variables en Python son en realidad referencias, pero en el sentido de que son punteros, no referencias constantes detrás de escena como los
type& name
argumentos de C ++ . Dado que este concepto no existe en Python, implementar la iteración por referencia, ¡y mucho menos convertirlo en el predeterminado! - requerirá agregar más complejidad al bytecode.La
for
declaración de Python funciona no solo en matrices, sino en un concepto más general de generadores. Detrás de escena, Python llamaiter
a sus matrices para obtener un objeto que, cuando lo llamanext
, devuelve el siguiente elemento oraise
saStopIteration
. Hay varias formas de implementar generadores en Python, y hubiera sido mucho más difícil implementarlos para la iteración por referencia.fuente
*it = ...
, pero este tipo de sintaxis ya indica que está modificando algo en otro lugar, lo que hace que la razón # 1 sea un problema menor. Las razones # 2 y # 3 no se aplican también, porque en C ++ la copia es costosa y existe el concepto de variables de referencia. En cuanto a la razón # 4, la capacidad de devolver una referencia permite una implementación simple para todos los casos.Ninguna de las respuestas aquí le brinda un código con el que trabajar para ilustrar realmente por qué sucede esto en Python land. Y esto es divertido de ver en un enfoque más profundo, así que aquí va.
La razón principal por la que esto no funciona como espera es porque en Python, cuando escribe:
no está haciendo lo que crees que está haciendo. Los enteros son inmutables. Esto se puede ver cuando observa qué es realmente el objeto en Python:
La función id representa un valor único y constante para un objeto en su vida útil. Conceptualmente, se asigna libremente a una dirección de memoria en C / C ++. Ejecutando el código anterior:
Esto significa que el primero
a
ya no es el mismo que el segundo.a
, porque sus identificaciones son diferentes. Efectivamente están en diferentes lugares en la memoria.Con un objeto, sin embargo, las cosas funcionan de manera diferente. He sobrescrito el
+=
operador aquí:Ejecutar esto da como resultado el siguiente resultado:
Tenga en cuenta que el atributo id en este caso es en realidad el mismo para ambas iteraciones, aunque el valor del objeto es diferente (también podría encontrar el
id
valor int que tiene el objeto, que estaría cambiando a medida que muta, porque los enteros son inmutables)Compare esto con cuando ejecuta el mismo ejercicio con un objeto inmutable:
Esto produce:
Algunas cosas aquí para notar. Primero, en el ciclo con el
+=
, ya no está agregando al objeto original. En este caso, dado que los ints se encuentran entre los tipos inmutables en Python , python usa una identificación diferente. También es interesante notar que Python usa el mismo subyacenteid
para múltiples variables con el mismo valor inmutable:tl; dr - Python tiene un puñado de tipos inmutables, que causan el comportamiento que ves. Para todos los tipos mutables, su expectativa es correcta.
fuente
La respuesta de @Idan hace un buen trabajo al explicar por qué Python no trata la variable de bucle como un puntero de la manera que podría hacerlo en C, pero vale la pena explicar con mayor profundidad cómo se descomprimen los fragmentos de código, como en Python muchos bits aparentemente simples. de código en realidad serán llamadas a métodos integrados . Para tomar tu primer ejemplo
Hay dos cosas para desempaquetar: la
for _ in _:
sintaxis y la_ += _
sintaxis. Para tomar el bucle for primero, al igual que otros lenguajes, Python tiene unfor-each
bucle que es esencialmente azúcar de sintaxis para un patrón iterador. En Python, un iterador es un objeto que define un.__next__(self)
método que devuelve el elemento actual en la secuencia, avanza al siguiente y generará unStopIteration
cuando no haya más elementos en la secuencia. Un Iterable es un objeto que define un.__iter__(self)
método que devuelve un iterador.(Nota: an
Iterator
también es anIterable
y se devuelve de su.__iter__(self)
método).Python generalmente tendrá una función incorporada que delega al método personalizado de doble subrayado. Entonces tiene
iter(o)
cuál resuelveo.__iter__()
ynext(o)
eso resuelveo.__next__()
. Tenga en cuenta que estas funciones incorporadas a menudo intentarán una definición predeterminada razonable si el método al que delegarían no está definido. Por ejemplo,len(o)
generalmente se resuelveo.__len__()
pero, si ese método no está definido, lo intentaráiter(o).__len__()
.Un bucle se define esencialmente en términos de
next()
,iter()
y estructuras de control más básico. En general el códigoserá desempaquetado a algo como
Entonces en este caso
se desempaqueta a
La otra mitad de esto es
i += 1
. En general%ASSIGN% += %EXPR%
se desempaqueta%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Aquí__iadd__(self, other)
hace una adición in situ y se devuelve.(Nota: este es otro caso en el que Python elegirá una alternativa si el método principal no está definido. Si el objeto no se implementa
__iadd__
, volverá a funcionar__add__
. De hecho, en este caso loint
hace, ya que no se implementa__iadd__
, lo cual tiene sentido porque son inmutables y, por lo tanto, no se pueden modificar en su lugar).Entonces su código aquí se ve así
donde podemos definir
Hay un poco más en su segundo código. Las dos cosas nuevas que necesitamos saber es que
%ARG%[%KEY%] = %VALUE%
se desempaqueta(%ARG%).__setitem__(%KEY%, %VALUE%)
y%ARG%[%KEY%]
se desempaqueta(%ARG%).__getitem__(%KEY%)
. Al reunir este conocimiento, nosa[ix] += 1
descomprimimosa.__setitem__(ix, a.__getitem__(ix).__add__(1))
(de nuevo: en__add__
lugar de__iadd__
porque__iadd__
no se implementa por ints). Nuestro código final se ve así:Para responder realmente su pregunta de por qué el primero no modifica la lista, mientras que la segunda lo hace, en nuestro primer fragmento que estamos recibiendo
i
desdenext(_a_iter)
que los mediosi
serán unaint
. Comoint
no se puede modificar en su lugar,i += 1
no hace nada a la lista. En nuestro segundo caso, nuevamente no estamos modificando elint
pero estamos modificando la lista llamando__setitem__
.La razón de todo este elaborado ejercicio es porque creo que enseña la siguiente lección sobre Python:
Los métodos de subrayado doble son un obstáculo al comenzar, pero son esenciales para respaldar la reputación de "pseudocódigo ejecutable" de Python. Un programador decente de Python tendrá un conocimiento profundo de estos métodos y cómo se invocan y los definirá donde sea que tenga sentido hacerlo.
Editar : @deltab corrigió mi uso descuidado del término "colección".
fuente
__len__
y__contains__
+=
funciona de manera diferente en función de si el valor actual es mutable o inmutable . Esta fue la razón principal por la que tardó mucho tiempo en implementarse en Python, ya que los desarrolladores de Python temían que fuera confuso.Si
i
es un int, entonces no se puede cambiar ya que los ints son inmutables y, por lo tanto, si el valor de losi
cambios debe necesariamente apuntar a otro objeto:Sin embargo, si el lado izquierdo es mutable , entonces + = puede cambiarlo; como si fuera una lista:
En su bucle for, se
i
refiere a cada elemento dea
a su vez. Si esos son enteros, entonces se aplica el primer caso, y el resultadoi += 1
debe ser que se refiere a otro objeto entero. La lista,a
por supuesto, todavía tiene los mismos elementos que siempre tuvo.fuente
i = 1
establecei
en un objeto entero inmutable, entoncesi = []
debería establecersei
en un objeto de lista inmutable. En otras palabras, ¿por qué los objetos enteros son inmutables y los objetos de lista mutables? No veo ninguna lógica detrás de esto.list
implementa métodos que cambian su contenido,int
no.[]
es un objeto de lista mutable yi = []
hagamosi
referencia a ese objeto.+=
operador / método para comportarse de manera similar (principio de menor sorpresa) para ambos tipos: cambiar el objeto original o devolver una copia modificada para los enteros y las listas.+=
es sorprendente en Python, pero se consideró que las otras opciones que mencionas también habrían sido sorprendentes, o al menos menos prácticas (no se puede cambiar el objeto original con el tipo de valor más común) usa + = con, ints. Y copiar una lista completa es mucho más costoso que mutarlo, Python no copia cosas como listas y diccionarios a menos que se le indique explícitamente). Fue un gran debate en aquel entonces.El bucle aquí es algo irrelevante. Al igual que los parámetros o argumentos de la función, configurar un bucle for como ese es esencialmente una asignación elegante.
Los enteros son inmutables. La única forma de modificarlos es creando un nuevo entero y asignándolo al mismo nombre que el original.
La semántica de Python para el mapa de asignación directamente en C (no sorprende dado los punteros PyObject * de CPython), con la única advertencia de que todo es un puntero, y no se le permite tener punteros dobles. Considere el siguiente código:
¿Lo que pasa? Se imprime
1
. ¿Por qué? En realidad, es aproximadamente equivalente al siguiente código C:En el código C, es obvio que el valor de no
a
se ve afectado por completo.En cuanto a por qué las listas parecen funcionar, la respuesta es básicamente que estás asignando el mismo nombre. Las listas son mutables. La identidad del objeto nombrado
a[0]
cambiará, peroa[0]
sigue siendo un nombre válido. Puede verificar esto con el siguiente código:Pero, esto no es especial para las listas. Reemplace
a[0]
ese código cony
y obtendrá exactamente el mismo resultado.fuente