¿Por qué puedo usar el mismo nombre para el iterador y la secuencia en un bucle for de Python?


79

Esta es más una cuestión conceptual. Recientemente vi un fragmento de código en Python (funcionó en 2.7, y también podría haberse ejecutado en 2.5) en el que un forbucle usaba el mismo nombre tanto para la lista que se estaba iterando como para el elemento en la lista , lo que me parece una mala práctica y algo que no debería funcionar en absoluto.

Por ejemplo:

x = [1,2,3,4,5]
for x in x:
    print x
print x

Rendimientos:

1
2
3
4
5
5

Ahora, tiene sentido para mí que el último valor impreso sería el último valor asignado ax desde el ciclo, pero no entiendo por qué podría usar el mismo nombre de variable para ambas partes del forciclo y tener Funciona según lo previsto. ¿Están en diferentes ámbitos? ¿Qué está pasando debajo del capó que permite que algo como esto funcione?


1
Como un interesante experimento mental: defina una función printAndReturn que tome un argumento, lo imprima y devuelva is. Luego for i in printAndReturn [1,2,3,4,5] …, ¿cuántas veces se deben [1,2,3,4,5]imprimir?
Joshua Taylor

1
Una nota sobre el alcance, ya que nadie más lo mencionó directamente: Python tiene un alcance a nivel de función, pero nada como el alcance a nivel de bloque de C. Entonces, el interior y el exterior del forbucle tienen el mismo alcance.
Izkata

Corrigí el título de la pregunta, ya que era un poco engañoso. El hecho de que algo sea una mala práctica no significa que no funcione. Puede ser que sea más propenso a errores, o difícil de leer / mantener, etc.
nico

Gracias. Estoy completamente de acuerdo en que era un mal título, simplemente no sabía cómo llamarlo inicialmente.
Gustav

esto también funciona en php for ($x as $x)pero es un código feo IMO
chiliNUT

Respuestas:


67

Que disnos dice:

Python 3.4.1 (default, May 19 2014, 13:10:29)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from dis import dis
>>> dis("""x = [1,2,3,4,5]
... for x in x:
...     print(x)
... print(x)""")

  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 LOAD_CONST               2 (3)
              9 LOAD_CONST               3 (4)
             12 LOAD_CONST               4 (5)
             15 BUILD_LIST               5
             18 STORE_NAME               0 (x)

  2          21 SETUP_LOOP              24 (to 48)
             24 LOAD_NAME                0 (x)
             27 GET_ITER
        >>   28 FOR_ITER                16 (to 47)
             31 STORE_NAME               0 (x)

  3          34 LOAD_NAME                1 (print)
             37 LOAD_NAME                0 (x)
             40 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             43 POP_TOP
             44 JUMP_ABSOLUTE           28
        >>   47 POP_BLOCK

  4     >>   48 LOAD_NAME                1 (print)
             51 LOAD_NAME                0 (x)
             54 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             57 POP_TOP
             58 LOAD_CONST               5 (None)
             61 RETURN_VALUE

Los bits clave son las secciones 2 y 3: cargamos el valor de x( 24 LOAD_NAME 0 (x)) y luego obtenemos su iterador ( 27 GET_ITER) y comenzamos a iterar sobre él ( 28 FOR_ITER). Python nunca vuelve a cargar el iterador nuevamente .

Aparte: no tendría ningún sentido hacerlo, ya que ya tiene el iterador, y como señala Abhijit en su respuesta , la Sección 7.3 de la especificación de Python realmente requiere este comportamiento).

Cuando el nombre xse sobrescribe para apuntar a cada valor dentro de la lista anteriormente conocida como xPython no tiene problemas para encontrar el iterador porque nunca necesita mirar el nombre xnuevamente para finalizar el protocolo de iteración.


8
"Python nunca vuelve a cargar el iterador de nuevo (no tendría sentido hacerlo, ya que ya tiene el iterador)". Esto describe el comportamiento que observa en el desmontaje, pero no dice si ese tiene que ser el caso o no; La respuesta de Abhijit cita el manual donde esto realmente se especifica.
Joshua Taylor

42

Usando su código de ejemplo como referencia principal

x = [1,2,3,4,5]
for x in x:
    print x
print x

Me gustaría que hiciera referencia a la sección 7.3. La declaración for en el manual

Extracto 1

La lista de expresiones se evalúa una vez; debería producir un objeto iterable. Se crea un iterador para el resultado de expression_list.

Lo que significa es que su variable x, que es un nombre simbólico de un objeto list: [1,2,3,4,5]se evalúa como un objeto iterable. Incluso si la variable, la referencia simbólica cambia su lealtad, ya que la lista de expresiones no se evalúa nuevamente, no hay impacto en el objeto iterable que ya ha sido evaluado y generado.

Nota

  • Todo en Python es un objeto, tiene un identificador, atributos y métodos.
  • Las variables son Nombre simbólico, una referencia a uno y solo un objeto en una instancia determinada.
  • Las variables en tiempo de ejecución pueden cambiar su lealtad, es decir, pueden referirse a algún otro objeto.

Extracto 2

Luego, la suite se ejecuta una vez para cada elemento proporcionado por el iterador, en el orden de índices ascendentes.

Aquí, la suite se refiere al iterador y no a la lista de expresiones. Entonces, para cada iteración, el iterador se ejecuta para producir el siguiente elemento en lugar de referirse a la lista de expresiones original.


5

Es necesario que funcione de esta manera, si lo piensa. La expresión de la secuencia de un forbucle podría ser cualquier cosa:

binaryfile = open("file", "rb")
for byte in binaryfile.read(5):
    ...

No podemos consultar la secuencia en cada paso a través del ciclo, o aquí terminaríamos leyendo del siguiente lote de 5 bytes la segunda vez. Naturalmente, Python debe almacenar de alguna manera el resultado de la expresión de forma privada antes de que comience el ciclo.


¿Están en diferentes ámbitos?

No. Para confirmar esto, puede mantener una referencia al diccionario de alcance original ( locals () ) y notar que, de hecho, está usando las mismas variables dentro del ciclo:

x = [1,2,3,4,5]
loc = locals()
for x in x:
    print locals() is loc  # True
    print loc["x"]  # 1
    break

¿Qué está pasando debajo del capó que permite que algo como esto funcione?

Sean Vieira mostró exactamente lo que está sucediendo bajo el capó, pero para describirlo en un código Python más legible, su forciclo es esencialmente equivalente a este whileciclo:

it = iter(x)
while True:
    try:
        x = it.next()
    except StopIteration:
        break
    print x

Esto es diferente del enfoque tradicional de indexación para la iteración que vería en versiones anteriores de Java, por ejemplo:

for (int index = 0; index < x.length; index++) {
    x = x[index];
    ...
 }

Este enfoque fallaría cuando la variable de elemento y la variable de secuencia son iguales, porque la secuencia xya no estaría disponible para buscar el índice siguiente después de que la primera vez xse reasigne al primer elemento.

Sin embargo, con el primer enfoque, la primera línea ( it = iter(x)) solicita un objeto iterador que es realmente responsable de proporcionar el siguiente elemento a partir de ese momento. xYa no es necesario acceder directamente a la secuencia a la que apuntaba originalmente.


4

Es la diferencia entre una variable (x) y el objeto al que apunta (la lista). Cuando comienza el ciclo for, Python toma una referencia interna al objeto al que apunta x. Utiliza el objeto y no lo que x hace referencia en un momento dado.

Si reasigna x, el ciclo for no cambia. Si x apunta a un objeto mutable (p. Ej., Una lista) y usted cambia ese objeto (p. Ej., Elimina un elemento), los resultados pueden ser impredecibles.


3

Básicamente, el ciclo for toma la lista xy luego, almacenándola como una variable temporal, vuelve a asignar xa cada valor en esa variable temporal. Por lo tanto, xahora es el último valor de la lista.

>>> x = [1, 2, 3]
>>> [x for x in x]
[1, 2, 3]
>>> x
3
>>> 

Como en esto:

>>> def foo(bar):
...     return bar
... 
>>> x = [1, 2, 3]
>>> for x in foo(x):
...     print x
... 
1
2
3
>>> 

En este ejemplo, xse almacena foo()como bar, por lo que, aunque xse está reasignando, todavía existe (ed) en foo()para que podamos usarlo para activar nuestro forbucle.


En realidad, en el último ejemplo, no creo que xesté siendo reasignado. Se barcrea una variable local fooy se le asigna el valor de x. fooluego devuelve ese valor en forma de un objeto que se usa en la forcondición. Por tanto, la variable xnunca se reasignó en el segundo ejemplo. Aunque estoy de acuerdo con el primero.
Tonio

Sin xembargo, @Tonio sigue siendo la variable de iteración y, por lo tanto, toma un nuevo valor para cada ciclo. Después del bucle, xes igual a 3en ambos casos.
Peter Gibson

@PeterGibson Tienes toda la razón, se me escapó la atención.
Tonio

Si fuera una "nueva variable" dentro del ciclo, entonces, ¿cómo es posible que el ciclo xmantenga 3y not [1,2,3] `?
Joshua Taylor

@JoshuaTaylor En Python, la variable de índice de bucle tiene un ámbito léxico en el bloque en el que se produjo el bucle for.
HennyH

1

xya no se refiere a la xlista original , por lo que no hay confusión. Básicamente, Python recuerda que está iterando sobre la xlista original , pero tan pronto como comienzas a asignar el valor de iteración (0,1,2, etc.) al nombre x, ya no se refiere a la xlista original . El nombre se reasigna al valor de iteración.

In [1]: x = range(5)

In [2]: x
Out[2]: [0, 1, 2, 3, 4]

In [3]: id(x)
Out[3]: 4371091680

In [4]: for x in x:
   ...:     print id(x), x
   ...:     
140470424504688 0
140470424504664 1
140470424504640 2
140470424504616 3
140470424504592 4

In [5]: id(x)
Out[5]: 140470424504592

2
No hace tanto una copia de la lista de rangos (ya que los cambios en la lista aún producirían un comportamiento indefinido en la iteración). xsimplemente deja de referirse a la lista de rangos y en su lugar se le asignan los nuevos valores de iteración. La lista de rangos todavía existe intacta. Si observa el valor de xafter the loop, será4
Peter Gibson

"x ya no se refiere a la x original" xnunca se menciona x; xreferido a una secuencia. Luego se refirió 1, luego a 2, etc.
Joshua Taylor
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.