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
.