遍历 list 时对原对象 remove 操作遇到的坑

Table of Contents

有这样一个函数,对传入的 list 参数,过滤掉其中一些元素,如:

def remove_some_ruits(food_list):
    remove_target_tags = ("苹果", "甘蔗", "葡萄", "枣子", "柚子", "桃子")

    for food in food_list:
        if food in remove_target_tags:
            food_list.remove(food)

    return food_list

如果调用:

foods = ["苹果", "柚子", "牛排", "冰淇淋"]
print(remove_some_ruits(foods))

预期输出应该是:

['牛排', '冰淇淋']

但实际结果为:

['柚子', '牛排', '冰淇淋']

简而言之,Python 在循环列表时内部维护了一个索引值,每遍历一个元素索引值就会加1;但是在遍历过程中擅自 remove 了元素并不会导致索引值减 1,所以删除 foods 的第一个元素“苹果”后,索引值为 1(从 0 开始),此时 foods 下一个值就指向了“牛排”,而跳过了“柚子”。

1. 深入研究

要弄清原理,只有去读 Python 的源码,Objects/listobject.c 中定义了一个专门用于迭代时用记录索引信息的 listiterobject 结构体:

typedef struct {
    PyObject_HEAD
    Py_ssize_t it_index;        /* 索引值 */
    PyListObject *it_seq;       /* 迭代的 list 对象 */
} listiterobject;

涉及到迭代 list 的相关代码均在 listobject.c 中。

迭代一个 list 对象时,先调用 list_iter 函数创建迭代对象,然后不断调用 listiter_next 取元素值。

list_iter 会创建一个 listiterobject 对象,并初始索引值为 0,实现逻辑如下:

static PyObject *
list_iter(PyObject *seq)
{
    listiterobject *it;
    ...
    it->it_index = 0;           /* 初始化索引值为 0 */
    ...
    it->it_seq = (PyListObject *)seq; /* 初始要迭代的 list 对象 */
    ...
    return (PyObject *)it;
}

listiter_next 负责根据当前索引值来取元素值,逻辑如下:

static PyObject *
listiter_next(listiterobject *it)
{
    PyListObject *seq;
    PyObject *item;

    ...
    seq = it->it_seq;

    /* 如果对象为空就返回 */
    if (seq == NULL)
        return NULL;
    ...
    if (it->it_index < PyList_GET_SIZE(seq)) {
        item = PyList_GET_ITEM(seq, it->it_index); /* 根据索引值来获取元素值 */
        ++it->it_index;         /* 索引值加 1 */
        Py_INCREF(item);
        return item;
    }

    it->it_seq = NULL;
    Py_DECREF(seq);
    return NULL;
}

前面的例子中,调用 remove 方法并不会影响 it->it_index 的索引值,因此导致了跳过元素。