1 前言

我记得之前在python使用for遍历list时不建议删除和插入内部元素,原因之一是可能导致原来迭代器失效,python报错抛出异常。但最近写的时候发现又没有这个问题了,一时之间不清楚是我记错了现象还是记错了用法。

于是我做了一些实验,翻了一些源码,分析一下原因。

2 使用迭代器遍历时插入

迭代器遍历在python里就很简单,举一个简单的例子:

a = [5, 6, 7, 8, 9]
for i in a:
    print(i)

输出:

5
6
7
8
9

2.1 尾部插入

那在上面的基础上,我们改一下代码,希望在访问到元素7的时候在尾部追加元素10,我们先这么写:

a = [5, 6, 7, 8, 9]
for i in a:
    print(i)
    if i == 7:
        a.append(10)

输出:

5
6
7
8
9
10

一个非常有意思的事情发生了,我们看到元素10被迭代器访问并输出到控制台中。由此我们得到一个现象:

list使用迭代器访问时,在尾部插入元素,迭代器也可以访问到该元素。

也就是说,list的迭代器在插入后它的下限(end)在某种机制下更新了,以至于能访问到最后一个新插入的元素。

2.2 头部插入

那我们换一个位置插入试试,这次我们把目标放到了头部:

a = [5, 6, 7, 8, 9]
for i in a:
    print(i)
    if i == 7:
        a.insert(0, 4)

输出:

5
6
7
7
7
7
7
7
...

这个时候我们发现迭代器好像到7就不走了,这就有点反直觉了,一般我认为迭代器是指向元素7的,那么它在下一次遍历时应该会next指向8,与列表前是否插入元素无关才对。

2.3 源码分析

为了明白上面头插和尾插的异常现象,我去翻了一下python3.7.9的源码(https://www.python.org/ftp/python/3.7.9/Python-3.7.9.tgz),并翻到了list的迭代器实现。

https://github.com/python/cpython/blob/13c94747c74437e594b7fc242ff7da668e81887c/Objects/listobject.c#L3109

首先,我们需要知道python的所有类型、对象都是PyTypeObject类型衍生出来的,就比如list的迭代器类型定义:

typedef struct {
    PyObject_HEAD
    Py_ssize_t it_index;
    PyListObject *it_seq; /* Set to NULL when iterator is exhausted */
} listiterobject;
 
static void listiter_dealloc(listiterobject *);
static int listiter_traverse(listiterobject *, visitproc, void *);
static PyObject *listiter_next(listiterobject *);
static PyObject *listiter_len(listiterobject *);
static PyObject *listiter_reduce_general(void *_it, int forward);
static PyObject *listiter_reduce(listiterobject *);
static PyObject *listiter_setstate(listiterobject *, PyObject *state);
 
PyDoc_STRVAR(length_hint_doc, "Private method returning an estimate of len(list(it)).");
PyDoc_STRVAR(reduce_doc, "Return state information for pickling.");
PyDoc_STRVAR(setstate_doc, "Set state information for unpickling.");
 
static PyMethodDef listiter_methods[] = {
    {"__length_hint__", (PyCFunction)listiter_len, METH_NOARGS, length_hint_doc},
    {"__reduce__", (PyCFunction)listiter_reduce, METH_NOARGS, reduce_doc},
    {"__setstate__", (PyCFunction)listiter_setstate, METH_O, setstate_doc},
    {NULL,              NULL}           /* sentinel */
};
 
PyTypeObject PyListIter_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "list_iterator",                            /* tp_name */
    sizeof(listiterobject),                     /* tp_basicsize */
    0,                                          /* tp_itemsize */
    /* methods */
    (destructor)listiter_dealloc,               /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    0,                                          /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
    0,                                          /* tp_doc */
    (traverseproc)listiter_traverse,            /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    PyObject_SelfIter,                          /* tp_iter */
    (iternextfunc)listiter_next,                /* tp_iternext */
    listiter_methods,                           /* tp_methods */
    0,                                          /* tp_members */
};

我们注意到该迭代器的tp_iternext部分赋值了一个iternextfunc类型的listiter_next函数,我们看一下它的实现:

static PyObject *
listiter_next(listiterobject *it)
{
    PyListObject *seq;
    PyObject *item;
 
    assert(it != NULL);
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;
    assert(PyList_Check(seq));
 
    if (it->it_index < PyList_GET_SIZE(seq)) {
        item = PyList_GET_ITEM(seq, it->it_index);
        ++it->it_index;
        Py_INCREF(item);
        return item;
    }
 
    it->it_seq = NULL;
    Py_DECREF(seq);
    return NULL;
}

这部分逻辑很简单,就是从it迭代器中获取it_index(这个就是list数组元素下标),如果it_indexlistsize小,那么通过下标访问获取到元素item,将it_index加1,最终将item返回。

我们可以注意到,这里的迭代器访问本质上就是下标访问,这也能解释上面尾插和头插的奇异现象了:

  • 对于尾插来说,每次for迭代器遍历时都会判断if (it->it_index < PyList_GET_SIZE(seq)),也就是每次遍历都会校验当前下标和list的大小,这样当遍历的时候插入元素,也可以正常访问到最后一个元素。
  • 对于头插来说,每次迭代器遍历都是下标访问,那么原来元素7的下标是2,头插后下标变成3,此时迭代器中的it_index仍是3,还是访问元素7,这样一直死循环。

3 举一反三

3.1 删除元素

那基于原理,我们可以认为从list中删除元素理论上也会出现类似的问题,我们来一个试一试:

a = [5, 6, 7, 8, 9]
for i in a:
    print(i)
    if i == 7:
        a.remove(7)

输出

5
6
7
9

我们注意到元素8没了,说明到7的时候,我们把元素7删除,此时下标2的元素变成了8,迭代器在下次遍历时又+1,最终指向了9。

4 总结

总而言之,pythonlist迭代器是一个基于下标随机访问的封装产物,这就意味着我们在对list迭代器遍历时应同样考虑这些“特性”,避免写一堆奇怪的bug。