【问题标题】:How does Python manage a 'for' loop internally?Python 如何在内部管理“for”循环?
【发布时间】:2017-08-29 14:42:47
【问题描述】:

我正在努力学习 Python,并开始玩一些代码:

a = [3,4,5,6,7]
for b in a:
    print(a)
    a.pop(0)

输出是:

[3, 4, 5, 6, 7]
[4, 5, 6, 7]
[5, 6, 7]

我知道这不是在循环时更改数据结构的好习惯,但我想了解 Python 在这种情况下如何管理迭代器。

主要问题是:如果我正在更改 a 的状态,它如何知道它必须完成循环?

【问题讨论】:

  • 你也应该打印b,这会给你更多的线索
  • Python 按需循环遍历列表。意味着在每次迭代中,它将相应的项目分配给一次性变量并继续迭代,直到遇到列表末尾或引发IndexError。回答python如何知道如果您要更改列表,它必须完成循环,这一切都基于遇到最后一项或提出IndexError。通过更改列表,您可以缩短或延长循环。
  • 这可能会有所帮助:stackoverflow.com/questions/13939341/…
  • Python 不承诺这种行为。未来的版本可能会给你一个类似字典的RuntimeError: list changed size during iteration

标签: python for-loop data-structures


【解决方案1】:

您不应该这样做的原因正是因为您不必依赖迭代的实现方式。

但回到问题。 Python 中的列表是数组列表。它们代表一块连续的已分配内存,而不是每个元素独立分配的链表。因此,Python 的列表,就像 C 中的数组一样,针对随机访问进行了优化。换句话说,从元素 n 到元素 n+1 的最有效方法是直接访问元素 n+1(通过调用 mylist.__getitem__(n+1)mylist[n+1])。

因此,列表的__next__(每次迭代调用的方法)的实现就像您期望的一样:当前元素的索引首先设置为0,然后在每次迭代后增加。

在您的代码中,如果您还打印b,您将看到发生这种情况:

a = [3,4,5,6,7]
for b in a:
    print a, b
    a.pop(0)

结果:

[3, 4, 5, 6, 7] 3
[4, 5, 6, 7] 5
[5, 6, 7] 7

因为:

  • 在第 0 次迭代时,a[0] == 3
  • 在第 1 次迭代中,a[1] == 5
  • 在第 2 次迭代中,a[2] == 7
  • 在第 3 次迭代中,循环结束 (len(a) < 3)

【讨论】:

    【解决方案2】:

    kjaquier 和 Felix 讨论了迭代器协议,我们可以在您的案例中看到它的作用:

    >>> L = [1, 2, 3]
    >>> iterator = iter(L)
    >>> iterator
    <list_iterator object at 0x101231f28>
    >>> next(iterator)
    1
    >>> L.pop()
    3
    >>> L
    [1, 2]
    >>> next(iterator)
    2
    >>> next(iterator)
    Traceback (most recent call last):
      File "<input>", line 1, in <module>
    StopIteration
    

    由此我们可以推断出list_iterator.__next__ 的代码行为类似于:

    if self.i < len(self.list):
        return self.list[i]
    raise StopIteration
    

    它不会天真地获取该项目。这会引发IndexError,它会冒泡到顶部:

    class FakeList(object):
        def __iter__(self):
            return self
    
        def __next__(self):
            raise IndexError
    
    for i in FakeList():  # Raises `IndexError` immediately with a traceback and all
        print(i)
    

    确实,在 the CPython source 中查看 listiter_next(感谢 Brian Rodriguez):

    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;
    

    虽然我不知道return NULL; 最终如何转换为StopIteration

    【讨论】:

    • "虽然我不知道如何返回 NULL;最终转化为 StopIteration" 它没有。 StopIterationPyIter_Next 中变为 NULL (可能在其他可能自己实现迭代器协议的地方。没有进一步检查)
    • @3Doubloons 我不确定你的意思,NULL 确实 转换为StopIteration。在listiter_next 的被调用者中对其进行了显式测试,如果没有发生其他错误,则设置异常。
    • @JimFasarakisHilliard:我在 PyIter_Next (以及 builtins.all 和 list.extend 的第二意见)中读到的是,当 tp_iternext 返回 NULL 时,C 代码会检查是否存在异常。如果有并且它是一个 StopIteration,它会清除它(因为这是正常的)。基于此(并且仅此),在我看来,__next__ 的 C 实现只是简单地返回 NULL 而不设置异常停止。只有 Python 代码需要引发 StopIteration,因为它不能返回 NULL(None 不是 NULL,它是 Py_None)
    • @3Doubloons tp_iternext 的插槽由设置 PyExc_StopIteration 的描述符填充,因此,即使设置为 tp_iternext 的实际函数没有设置异常,它被某些东西包裹确实如此。当你说NULL 不能转换为StopIteration 时,这就是我不同意的。 :-)
    • 啊,是的。我现在看到了。 wrap_next 隐藏在 typeobject.c 的深处,如果实际 tp_iternext 返回 NULL 且无异常,则引发 StopIteration。谜团解开
    【解决方案3】:

    我们可以通过一个小辅助函数foo轻松查看事件顺序:

    def foo():
        for i in l:
            l.pop()
    

    dis.dis(foo) 查看生成的 Python 字节码。剪掉不那么相关的操作码,您的循环执行以下操作:

              2 LOAD_GLOBAL              0 (l)
              4 GET_ITER
        >>    6 FOR_ITER                12 (to 20)
              8 STORE_FAST               0 (i)
    
             10 LOAD_GLOBAL              0 (l)
             12 LOAD_ATTR                1 (pop)
             14 CALL_FUNCTION            0
             16 POP_TOP
             18 JUMP_ABSOLUTE            6
    

    也就是说,它获取给定对象的iteriter(l) 是列表的专用迭代器对象)并循环直到FOR_ITER 发出停止的信号。添加多汁的部分,这是FOR_ITER 所做的:

    PyObject *next = (*iter->ob_type->tp_iternext)(iter);
    

    本质上是:

    list_iterator.__next__()
    

    this (finally*) 到达listiter_next,它在检查期间使用原始序列l 作为@Alex 执行索引检查。

    if (it->it_index < PyList_GET_SIZE(seq))
    

    当这失败时,NULL 被返回,这表明迭代已经完成。同时设置了一个StopIteration 异常,该异常在FOR_ITER 操作码代码中被静默抑制:

    if (!PyErr_ExceptionMatches(PyExc_StopIteration))
        goto error;
    else if (tstate->c_tracefunc != NULL)
        call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
    PyErr_Clear();  /* My comment: Suppress it! */
    

    因此,无论您是否更改列表,listiter_next 中的检查最终都会失败并执行相同的操作。

    *对于任何想知道的人,listiter_next 是一个描述符,所以有一个小函数包装它。在这种特定情况下,该函数是wrap_next,它确保在listiter_next 返回NULL 时将PyExc_StopIteration 设置为异常。

    【讨论】:

      【解决方案4】:

      AFAIK,for 循环使用迭代器协议。您可以手动创建和使用迭代器,如下所示:

      In [16]: a = [3,4,5,6,7]
          ...: it = iter(a)
          ...: while(True):
          ...:     b = next(it)
          ...:     print(b)
          ...:     print(a)
          ...:     a.pop(0)
          ...:
      3
      [3, 4, 5, 6, 7]
      5
      [4, 5, 6, 7]
      7
      [5, 6, 7]
      ---------------------------------------------------------------------------
      StopIteration                             Traceback (most recent call last)
      <ipython-input-16-116cdcc742c1> in <module>()
            2 it = iter(a)
            3 while(True):
      ----> 4     b = next(it)
            5     print(b)
            6     print(a)
      

      如果迭代器用尽(引发 StopIteration),则 for 循环停止。

      【讨论】:

        猜你喜欢
        • 2012-12-07
        • 2022-11-11
        • 1970-01-01
        • 1970-01-01
        • 2015-03-21
        • 2021-10-28
        • 2010-12-31
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多