Python for…in..遍历中的问题与原理分析
本文最后更新于 479 天前,其中的信息可能已经有所发展或是发生改变。

众所周知,Python提供的 for…in… 是一个很甜的语法糖,用来遍历列表甚至可能爽到螺旋升天。可是在用这个语法糖遍历列表时,有一个需要注意的地方,今天在项目中便遇到了。

for item in fileList:
    if os.path.isdir(item):
        fileList.remove(item)

这段遍历的代码始终都无法完全的删除掉全部的文件夹….

我用pdb打了断点,给入一个有四个文件夹元素的列表,结果最后发现:

这段代码只执行了两遍…

这是为什么呢?(我相信有很多同学看到这里就已经知道答案了,但是后面的分析如果有兴趣还是可以看看的)

稍微对Python有些了解的童鞋应该知道,Python的for…in…语法糖实质上是一个对于可迭代对象迭代的过程,可以用如下代码来说明:

>>> string = "你说我帅不帅"
>>> dir(string)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> strIter = string.__iter__()
>>> type(strIter)
<class 'str_iterator'>
>>> dir(strIter)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']
>>> while True:
...   try:
...     char = strIter.__next__()
...   except StopIteration:
...     break
...   print ("迭代:" + char)
...
迭代:你
迭代:说
迭代:我
迭代:帅
迭代:不
迭代:帅

上面代码都是在python命令行中敲得,应该大家都能看得懂。

在Python中,当遍历到末尾元素之后,会抛出StopIteration异常,这样,便能够知道该结束遍历了。

但是在Python中,为了使iterable的对象可以复用(可以产生多个iterator),每次获取到的iterator都是单独的,如果对于原对象(字符串,数组)进行修改,会得到不同的iterator:

>>> string = "你说我帅不帅"
>>> strIter = string.__iter__()
>>> id(strIter)
54967920
>>> string += ",帅"
>>> strIter = string.__iter__()
>>> id(strIter)
54967696
>>>

所以在循环过程中,如果对原数组进行了修改(如果添加元素则会访问不到,如果删除元素则会越界),例如下列代码:

testList = [1, 2, 3, 4]
for num in testList:
    if num > 0:
        testList.remove(num)
print (testList)

来,你们猜猜,这段代码的输出是什么?你以为是 [] ?去,把我上面写的东西再看一遍去。

因为你对于数组的修改,这个iterator对象并不知情,所以在遍历过程中发生了如下的事情:

  1. iterator得到了下标为 0 的元素,也就是 1
  2. 你从数组中移除了 1,新的数组为 [2, 3, 4]
  3. iterator以为你的数组还是4个元素,开始获取下标为 1 的元素,也就是 3 
  4. 你从数组中移除了 3 ,新的数组为 [2, 4]
  5. iterator试图从数组中获取下标为2的元素,结果它也不知道为什么,引发了IndexError

然后遍历过程结束了,你并没有如愿以偿地得到空列表,而是得到了 [2, 4] 这样一个奇怪的结果。iterator表示:

其实在迭代器内部有一个提示量:__length_hint__,在迭代的每一步该值都会减一(初始值为原对象的长度),如果在迭代过程中该值未达到 0 便停止了,那么说明出现了一点小意外:

-> while True:
(Pdb) listIter.__length_hint__()
4
(Pdb) list
  8      
  9      testList = [1, 2, 3, 4]
 10      listIter = testList.__iter__()
 11      pdb.set_trace()
 12  ->    while True:
 13          try:
 14              num = listIter.__next__()
 15              testList.remove(num)
 16              pdb.set_trace()
 17          except StopIteration:
(Pdb) continue
-> try:
(Pdb) listIter.__length_hint__()
2
(Pdb) continue
-> try:
(Pdb) listIter.__length_hint__()
0
(Pdb) continue [2, 4]

这是我用pdb打断点调出来的值,大家可以发现该值的变化规律。

那不是说好的只有出来StopIteration异常时候正常停止迭代吗,这个明显越界了啊?

我也不知道为什么,但是我费了一番功夫,在Python的源码中找到了这样的代码:

static PyObject *
iter_iternext(PyObject *iterator)
{
    seqiterobject *it;
    PyObject *seq;
    PyObject *result;

    assert(PySeqIter_Check(iterator));
    it = (seqiterobject *)iterator;
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;
    if (it->it_index == PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
                        "iter index too large");
        return NULL;
    }
    result = PySequence_GetItem(seq, it->it_index);
    if (result != NULL) {
        it->it_index++;
        return result;
    }
    if (PyErr_ExceptionMatches(PyExc_IndexError) ||
        PyErr_ExceptionMatches(PyExc_StopIteration))
    {
        PyErr_Clear();
        Py_DECREF(seq);
        it->it_seq = NULL;
    }
    return NULL;
}

最后终止迭代的引发条件多了一个 (PyExc_IndexError),所以当出现越界的时候也静默的终止了迭代。


所以最后的结论就是:

不要在遍历列表的过程中对原列表进行修改!

如果想要优雅的修改列表,请使用列表解析或者在遍历的原列表加上一个 [ : ],进行一份拷贝。

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇