I\'m trying to learn Python, and I started to play with some code:
a = [3,4,5,6,7]
for b in a:
print(a)
a.pop(0)
<
We can easily see the sequence of events by using a little helper function foo
:
def foo():
for i in l:
l.pop()
and dis.dis(foo)
to see the Python byte-code generated. Snipping away the not-so-relevant opcodes, your loop does the following:
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
That is, it get's the iter
for the given object (iter(l)
a specialized iterator object for lists) and loops until FOR_ITER
signals that it's time to stop. Adding the juicy parts, here's what FOR_ITER
does:
PyObject *next = (*iter->ob_type->tp_iternext)(iter);
which essentially is:
list_iterator.__next__()
this (finally*) goes through to listiter_next
which performs the index check as @Alex using the original sequence l
during the check.
if (it->it_index < PyList_GET_SIZE(seq))
when this fails, NULL
is returned which signals that the iteration has finished. In the meantime a StopIteration
exception is set which is silently suppressed in the FOR_ITER
op-code code:
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! */
so whether you change the list or not, the check in listiter_next
will ultimately fail and do the same thing.
*For anyone wondering, listiter_next
is a descriptor so there's a little function wrapping it. In this specific case, that function is wrap_next
which makes sure to set PyExc_StopIteration
as an exception when listiter_next
returns NULL
.