Python for 循环实际上是如何工作的?

How does the Python for loop actually work?

我很想了解 Python for 循环在幕后是如何工作的。我试着像下面的代码片段一样实现它,for 循环是如何实现的?

my_list = [1, 2, 3, 4, 5]

# list itself is iterable but not iterator. Make it an iterator
iter_list = iter(my_list)

while True:
    try:
       print(next(iter_list))
    except StopIteration:
       break

是的,这很好地近似了 for 循环构造的实现方式。它肯定匹配 for loop statement documentation:

The expression list is evaluated once; it should yield an iterable object. An iterator is created for the result of the expression_list. The suite is then executed once for each item provided by the iterator, in the order returned by the iterator. Each item in turn is assigned to the target list using the standard rules for assignments (see Assignment statements), and then the suite is executed. When the items are exhausted (which is immediately when the sequence is empty or an iterator raises a StopIteration exception), the suite in the else clause, if present, is executed, and the loop terminates.

您只错过了使用标准分配规则分配给目标列表部分;您必须使用 i = next(iter_list)print(i) 而不是直接打印 next() 调用的结果。

Python 源代码被编译为 字节码 ,然后解释器循环执行。您可以使用 dis module:

查看 for 循环的字节码
>>> import dis
>>> dis.dis('for i in mylist: pass')
  1           0 SETUP_LOOP              12 (to 14)
              2 LOAD_NAME                0 (mylist)
              4 GET_ITER
        >>    6 FOR_ITER                 4 (to 12)
              8 STORE_NAME               1 (i)
             10 JUMP_ABSOLUTE            6
        >>   12 POP_BLOCK
        >>   14 LOAD_CONST               0 (None)
             16 RETURN_VALUE

命名的各种操作码记录在同一个 dis 模块中,它们的实现可以在 CPython evaluation loop 中找到(寻找 TARGET(<opcode>) 开关目标);上面的操作码分解为:

  • SETUP_LOOP 12 标志着 套件 的开始,这是一个语句块,因此解释器知道在 break 的情况下跳转到哪里,以及在出现异常或 return 语句时需要进行哪些清理;清理操作码位于此操作码之后的 12 个字节的字节码处(因此此处 POP_BLOCK)。
  • LOAD_NAME 0 (mylist) 加载 mylist 变量值,将其放在堆栈的顶部(操作码描述中的 TOS)。
  • GET_ITER 对 TOS 上的对象调用 iter(),然后用结果替换 TOS。
  • FOR_ITER 4 在 TOS 迭代器上调用 next()。如果给出结果,则将其推送到 TOS。如果出现 StopIteration 异常,则从 TOS 中删除迭代器,并跳过 4 个字节的字节码到 POP_BLOCK 操作码。
  • STORE_NAME 1 获取 TOS 并将其放入命名变量中,这里是 i.
  • JUMP_ABSOLUTE 6 标志循环体结束;它告诉解释器返回到字节码偏移量 6,即上面的 FOR_ITER 指令。如果我们在循环中做了一些有趣的事情,那么这将发生在 STORE_NAME 之后,JUMP_ABSOLUTE.
  • 之前
  • POP_BLOCK 删除由 SETUP_LOOP 设置的块簿记并从堆栈中删除迭代器。

>> 标记是跳转目标,有视觉提示,可以在阅读跳转到它们的操作码行时更容易发现它们。