几乎每个关于该主题的教程和 SO 答案都坚持认为您永远不应该在迭代列表时修改列表,但如果代码有效,我不明白为什么这是一件坏事。例如:
while len(mylist) > 0:
print mylist.pop()
我错过了什么吗?
while len(mylist) > 0:
print mylist.pop()
您没有遍历列表。您每次都在检查原子条件。
还:
while len(mylist) > 0:
可以改写为:
while len(mylist):
可以重写为:
while mylist:
为什么你不应该在迭代列表时修改它的原因是,例如,你正在迭代一个 20 位数字的列表,如果你碰到一个偶数,你就会把它从列表中弹出并继续直到你有一个列表只是奇数。
现在,假设这是您的示例数据[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
,然后您开始对其进行迭代。第一次迭代,数字是1
你继续,下面的数字是2
你弹出它,冲洗并重复。您现在感觉应用程序工作正常,结果列表为[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
.
现在假设您的示例数据是[1, 2, 4, 5, 7, 8, 10, 11, 12, 13, 15, 15, 17, 18, 20]
,并且您运行与以前相同的代码,并在迭代原始列表时对其进行变异。您的结果列表[1, 4, 5, 7, 10, 11, 13, 15, 15, 17, 20]
显然不正确,因为列表中仍然包含偶数。
如果您打算在像这样迭代列表时改变列表
for elem in lst:
# mutate list in place
您应该将其更改为
for elem in lst[:]:
# mutate list in place
该[:]
语法创建一个新列表,它是原始列表的精确副本,因此您可以愉快地改变原始列表而不会影响您正在处理的内容,因为您不会因改变您正在处理的列表而产生任何意外的副作用迭代通过。
如果您的列表相当大,那么与其创建一个新列表并逐步执行它,不如考虑使用生成器表达式或为您的列表编写自己的生成器,这样您就不会浪费内存和 CPU 周期。
我将更详细地介绍为什么您不应该遍历列表。当然,我的意思是
for elt in my_list:
my_list.pop()
或类似的成语。
首先,我们需要考虑 Python 的for
循环是做什么的。因为你可以尝试迭代任何对象,Python 不一定知道如何迭代你给它的任何东西。所以有一个列表(呵呵)它试图做的事情来解决如何一个一个地呈现这些值。它做的第一件事是检查__iter__
对象上的方法,如果存在的话,就调用它。
这个调用的结果将是一个可迭代的对象;也就是一个有next
方法的。现在我们可以开始了:只需next
反复跟注,直到StopIteration
被加注。
为什么这很重要?好吧,因为该__iter__
方法实际上必须查看数据结构以查找值,并记住一些内部状态,以便它知道下一步该往哪里看。但是如果你改变了数据结构,那么__iter__
你就无法知道你一直在摆弄,所以它会愉快地继续尝试获取新数据。这在实践中意味着您可能会跳过列表中的元素。
通过查看源代码来证明这种说法总是很好的。来自listobject.c
:
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;
}
Py_DECREF(seq);
it->it_seq = NULL;
return NULL;
}
请特别注意,它确实模拟了 C 风格的for
循环,并it->it_index
扮演了索引变量的一部分。特别是,如果您从列表中删除一个项目,那么您将不会更新it_index
,因此您可能会跳过一个值。
您的代码不会遍历列表。
for i in mylist:
print mylist.pop()