while loop hacer for como python for-loop data-structures

loop - ¿Cómo maneja Python un bucle ''for'' internamente?



for en python (4)

AFAIK, el bucle for utiliza el protocolo iterador. Puede crear y usar manualmente el iterador de la siguiente manera:

In [16]: a = [3,4,5,6,7] ...: it = iter(a) ...: while(True): ...: b = next(it) ...: print(b) ...: print(a) ...: a.pop(0) ...: 3 [3, 4, 5, 6, 7] 5 [4, 5, 6, 7] 7 [5, 6, 7] --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-16-116cdcc742c1> in <module>() 2 it = iter(a) 3 while(True): ----> 4 b = next(it) 5 print(b) 6 print(a)

El bucle for se detiene si el iterador está agotado (provoca StopIteration ).

Estoy tratando de aprender Python, y comencé a jugar con un código:

a = [3,4,5,6,7] for b in a: print a a.pop(0)

Y la salida es:

[3, 4, 5, 6, 7] [4, 5, 6, 7] [5, 6, 7]

Sé que no es una buena práctica cambiar las estructuras de datos mientras lo hago en bucle, pero quiero entender cómo Python administra los iteradores en este caso.

La pregunta principal es: ¿cómo sabe que tiene que terminar el ciclo si estoy cambiando el estado de a ?


La razón por la que no debería hacer eso es precisamente para no tener que confiar en cómo se implementa la iteración.

Pero volvamos a la pregunta. Las listas en Python son listas de matrices. Representan una parte continua de la memoria asignada, a diferencia de las listas enlazadas en las que cada elemento se asigna de forma independiente. Por lo tanto, las listas de Python, como las matrices en C, están optimizadas para el acceso aleatorio. En otras palabras, la forma más eficiente de obtener del elemento n al elemento n + 1 es accediendo directamente al elemento n + 1 (llamando a mylist.__getitem__(n+1) o mylist[n+1] ).

Por lo tanto, la implementación de __next__ (el método llamado en cada iteración) para las listas es como usted esperaría: el índice del elemento actual se establece primero en 0 y luego se incrementa después de cada iteración.

En su código, si también imprime b , verá que esto sucede:

a = [3,4,5,6,7] for b in a: print a, b a.pop(0)

Resultado:

[3, 4, 5, 6, 7] 3 [4, 5, 6, 7] 5 [5, 6, 7] 7

Porque :

  • En la iteración 0, a[0] == 3 .
  • En la iteración 1, a[1] == 5 .
  • En la iteración 2, a[2] == 7 .
  • En la iteración 3, el bucle ha terminado ( len(a) < 3 )

Podemos ver fácilmente la secuencia de eventos usando una pequeña función auxiliar foo :

def foo(): for i in l: l.pop()

y dis.dis(foo) para ver el código de bytes de Python generado. Eliminando los códigos de operación no tan relevantes, su bucle hace lo siguiente:

2 LOAD_GLOBAL 0 (l) 4 GET_ITER >> 6 FOR_ITER 12 (to 20) 8 STORE_FAST 0 (i) 10 LOAD_GLOBAL 0 (l) 12 LOAD_ATTR 1 (pop) 14 CALL_FUNCTION 0 16 POP_TOP 18 JUMP_ABSOLUTE 6

Es decir, obtiene el iter para el objeto dado ( iter(l) un objeto iterador especializado para listas) y realiza un bucle hasta que FOR_ITER que es hora de detenerse. Agregando las partes jugosas, esto es lo que hace FOR_ITER :

PyObject *next = (*iter->ob_type->tp_iternext)(iter);

que en esencia es:

list_iterator.__next__()

esto (finalmente * ) pasa a listiter_next que realiza la comprobación de índice como @Alex utilizando la secuencia l original durante la comprobación.

if (it->it_index < PyList_GET_SIZE(seq))

cuando esto falla, se devuelve NULL que indica que la iteración ha finalizado. Mientras tanto, se establece una excepción StopIteration que se suprime de forma silenciosa en el código de FOR_ITER :

if (!PyErr_ExceptionMatches(PyExc_StopIteration)) goto error; else if (tstate->c_tracefunc != NULL) call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f); PyErr_Clear(); /* My comment: Suppress it! */

por lo tanto, ya sea que cambie la lista o no, la verificación en listiter_next fallará y hará lo mismo.

* Para cualquiera que se lo pregunte, listiter_next es un descriptor, así que hay una pequeña función que lo envuelve. En este caso específico, esa función es wrap_next que se asegura de establecer PyExc_StopIteration como una excepción cuando listiter_next devuelve NULL .


kjaquier y Felix han hablado sobre el protocolo del iterador, y podemos verlo en acción en su caso:

>>> L = [1, 2, 3] >>> iterator = iter(L) >>> iterator <list_iterator object at 0x101231f28> >>> next(iterator) 1 >>> L.pop() 3 >>> L [1, 2] >>> next(iterator) 2 >>> next(iterator) Traceback (most recent call last): File "<input>", line 1, in <module> StopIteration

De esto podemos inferir que list_iterator.__next__ tiene un código que se comporta algo como:

if self.i < len(self.list): return self.list[i] raise StopIteration

No consigue ingenuamente el artículo. Eso elevaría un IndexError que caería hasta la cima:

class FakeList(object): def __iter__(self): return self def __next__(self): raise IndexError for i in FakeList(): # Raises `IndexError` immediately with a traceback and all print(i)

De hecho, mirando listiter_next en la fuente CPython (gracias Brian Rodriguez):

if (it->it_index < PyList_GET_SIZE(seq)) { item = PyList_GET_ITEM(seq, it->it_index); ++it->it_index; Py_INCREF(item); return item; } Py_DECREF(seq); it->it_seq = NULL; return NULL;

Aunque no sé cómo return NULL; finalmente se traduce en una StopIteration .