Как Python управляет циклом "for" внутри?
Я пытаюсь изучить Python, и я начал играть с некоторым кодом:
a = [3,4,5,6,7]
for b in a:
print a
a.pop(0)
И результат:
[3, 4, 5, 6, 7]
[4, 5, 6, 7]
[5, 6, 7]
Я знаю, что не хорошая практика изменяет структуры данных, пока я зацикливаюсь на ней, но я хочу понять, как Python управляет итераторами в этом случае.
Главный вопрос: как он знает, что он должен закончить цикл, если я изменяю состояние a
?
Ответы
Ответ 1
kjaquier и Феликс говорили о протоколе итератора, и мы можем видеть его в действии в вашем случае:
>>> 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
Из этого можно заключить, что list_iterator.__next__
имеет код, который ведет себя как:
if self.i < len(self.list):
return self.list[i]
raise StopIteration
Он не наивно получает элемент. Это поднимет IndexError
, который будет пузыриться вверху:
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)
Действительно, глядя на listiter_next
в источник CPython (спасибо Брайан Родригес):
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;
Хотя я не знаю, как return NULL;
в конечном итоге переводит на StopIteration
.
Ответ 2
Причина, по которой вы не должны этого делать, заключается именно в том, что вам не нужно полагаться на то, как итерация реализована.
Но вернемся к вопросу. Списки в Python представляют собой списки массивов. Они представляют собой непрерывный фрагмент выделенной памяти, в отличие от связанных списков, в которых каждый элемент распределяется независимо. Таким образом, списки Python, такие как массивы на C, оптимизированы для случайного доступа. Другими словами, наиболее эффективным способом получить от элемента n до элемента n + 1 является прямой доступ к элементу n + 1 (путем вызова mylist.__getitem__(n+1)
или mylist[n+1]
).
Таким образом, реализация __next__
(метод, называемый каждой итерацией) для списков, точно так же, как вы ожидали: индекс текущего элемента сначала устанавливается на 0, а затем увеличивается после каждой итерации.
В вашем коде, если вы также распечатаете b
, вы увидите, что это происходит:
a = [3,4,5,6,7]
for b in a:
print a, b
a.pop(0)
Результат:
[3, 4, 5, 6, 7] 3
[4, 5, 6, 7] 5
[5, 6, 7] 7
Потому что:
- На итерации 0,
a[0] == 3
.
- На итерации 1,
a[1] == 5
.
- На итерации 2,
a[2] == 7
.
- На итерации 3 цикл завершен (
len(a) < 3
)
Ответ 3
Мы можем легко увидеть последовательность событий, используя небольшую вспомогательную функцию foo
:
def foo():
for i in l:
l.pop()
и dis.dis(foo)
, чтобы увидеть байт-код Python. Отбрасывая непротиворечивые коды операций, ваш цикл выполняет следующие действия:
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
То есть, он получает iter
для данного объекта (iter(l)
специализированный объект итератора для списков) и цикл до тех пор, пока FOR_ITER
не сообщит, что это время для остановки. Добавляя сочные части, вот что FOR_ITER
делает:
PyObject *next = (*iter->ob_type->tp_iternext)(iter);
который по существу есть:
list_iterator.__next__()
это (наконец, *) проходит до listiter_next
, который выполняет проверку индекса как @Alex с использованием исходной последовательности l
во время проверки.
if (it->it_index < PyList_GET_SIZE(seq))
когда это не удается, возвращается NULL
, который сигнализирует о завершении итерации. Тем временем устанавливается StopIteration
исключение, которое тихо подавляется в коде кода 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! */
поэтому, измените ли вы список или нет, проверка listiter_next
в конечном итоге завершится неудачей и сделает то же самое.
* Для всех, кто задается вопросом, listiter_next
является дескриптором, поэтому есть небольшая функция, обертывающая его. В этом конкретном случае эта функция wrap_next
, которая обязательно устанавливает PyExc_StopIteration
как исключение, когда listiter_next
возвращает NULL
.
Ответ 4
AFAIK, цикл for использует протокол итератора. Вы можете вручную создать и использовать итератор следующим образом:
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)
Цикл for останавливается, если итератор исчерпан (поднимается StopIteration
).