返回

文章详情

“for x in y”隐藏了什么 - 从零开始的代码

Hacker News2026年7月15日 12:57

有一些Python特性使用起来非常简单,以至于你会忘记它们实际上在做什么。考虑一下for x in y。你写一个列表、一个字符串或一个范围,Python会礼貌地一个一个地把项目递给你。没有索引变量,也没有边界检查。与C/C++中的i++或JavaScript中的forEach相比,Python的版本就是这样有效。很长一段时间,我把for x in y视为仅仅是一个“遍历这个东西”的语法,就这点已经足够了。然后我开始构建Memphis,我用Rust编写的Python解释器,最后我不得不停止随便应付,回答一个非常无礼的问题:for循环到底在做什么?事实证明,答案比我预期的要简单且复杂。 幻觉 让我们从一个小例子开始。for x in [10, 20, 30]: print(x) 运行它,你会得到熟悉的行为:一次一个值,按顺序排列。到目前为止,没有真正的惊喜。如果你刚开始学习Python,很容易得出这样的心理模型:Python查看列表,它逐个遍历元素,它将每个元素赋值给x,然后运行循环体。这种理解在行为上并不错误,但它隐藏了最重要的部分:Python并不是直接在遍历集合。它是在遍历一个迭代器。这个区别很重要,因为它解释了为何for可以在如此多不同类型的对象上工作,以及为何Python中的迭代感觉如此灵活。 隐藏的步骤 当Python看到这个:for x in [10, 20, 30]: print(x) 实际的故事更接近这样的:it = iter([10, 20, 30]) while True: try: x = next(it) print(x) except StopIteration: break 这就是问题的核心。for循环实际上是:请求一个迭代器,通过反复调用iter(...);请求下一个值,通过调用next(...);当next(...)抛出StopIteration时停止。for x in y语法只是一个漂亮的包装。一旦我理解了这一点,许多Python的行为就不再显得神秘。 你可以直接看到 我们可以不接触解释器内部就证明这一点。nums = [10, 20, 30] it = iter(nums) print(next(it)) print(next(it)) print(next(it)) 这将逐个打印列表元素。但如果我们走得一步太远:nums = [10, 20, 30] it = iter(nums) print(next(it)) print(next(it)) print(next(it)) print(next(it)) 运行第二段代码。最后一次next(it)抛出StopIteration,这正是循环知道它已经完成的方式。因此,循环并不是以某种特殊的一次性方式“读取列表”。它是使用你自己也可以使用的相同迭代器协议。 这为什么重要 乍一看,这可能看起来像一个在技术上是正确的事实,但并不是特别有用。我不认为情况是这样的。这个想法解释了为什么所有这些都可以工作:for x in [1, 2, 3]: print(x) for ch in "cat": print(ch) for n in range(3): print(n) 这些是完全不同类型的对象。一个列表不是一个字符串,也不是一个范围。然而,Python可以遍历它们所有,因为每个对象都可以生成一个迭代器。这也解释了为什么生成器与for如此自然地配合。生成器只是参与这个相同协议的另一种Python类型。待会儿我们会看到一个例子。一旦你明白这一点,Python的迭代模型就开始感觉统一,而不是魔法。 我必须实现的部分 在Memphis中,我不能简单地“A循环这个东西”。我必须决定运行时实际会做什么。从高层次来看,我的树遍历解释器通过以下步骤处理for循环:评估右侧表达式,一直调用iter(...);重复调用next(...);将每个返回值绑定到循环变量;在StopIteration被抛出时停止。写出来听起来几乎乏味,这我认为是Python规范的功劳。良好的抽象通常会在能够清晰命名后变得乏味。让我惊讶的不是for使用迭代,而是循环本身知道的东西是多么少。for循环不需要对列表、元组、范围、字符串、生成器或其他任何东西使用特殊逻辑。它只需要迭代器协议。具体对象处理细节。在完整的Python中,这向前更进一步:自定义对象也可以参与,通过实现__iter__()和__next__()。我在Memphis中仍在处理这部分,但这是这一设计最酷的结果之一。这是一个非常Pythonic的设计。 另一个可能帮助理解的例子 考虑这个:items = [1, 2, 3] it = iter(items) for x in it: print(x) for x in it: print("again:", x) 尝试自己运行一下。第二个循环不打印任何内容。这是因为迭代器在第一个循环中已经被耗尽了。for循环并没有“倒带”任何东西。它只是继续调用next(...),直到没有东西剩下。如果你想使这个区别更加具体,也可以检查类型:items = [1, 2, 3] it = iter(items) print(type(items)) print(type(it)) 这是一个微妙但重要的区别:可迭代对象通常可以给你一个新的迭代器。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡