Как 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).