遍历 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 的索引值,因此导致了跳过元素。