高原蓝 發表於 2025-7-17 09:20:00

Iterable:一个容易被忽视的Python编码细节

<h2 id="type-hints">Type hints</h2>
<p>近年来,越来越多的 Python 开发者愿意为变量声明类型了,变化非常明显。</p>
<pre><code class="language-python">def add(left, right):
    return left + right
</code></pre>
<pre><code class="language-python">from typing import TypeVar, Union

T = TypeVar('T', int, float)

def add_typed(left: T, right: T) -&gt; T:
    return left + right
</code></pre>
<p>虽然 type hints 并不会在运行时进行类型检查,但它们足以让 IDE 在运行前就报出不少类型风险,也让阅读代码的人有了更多思考空间,不至于迷失在“这到底是什么类型”的疑惑中。</p>
<h2 id="iterator-和-iterable-的问题">Iterator 和 Iterable 的问题</h2>
<p>Python 对 Iterator 的要求只有一条:</p>
<blockquote>
<p>An iterator object implements <code>__next__</code>, which is expected to return the next element of the iterable object that returned it, and to raise a <code>StopIteration</code> exception when no more elements are available.</p>
<p>一个迭代器对象需要实现 <code>__next__</code> 方法,该方法应当返回其所属可迭代对象的下一个元素;当没有更多元素可返回时,应抛出 <code>StopIteration</code> 异常。</p>
<p>-- Python Wiki</p>
</blockquote>
<p>Python 对 Iterable 的要求也很简单:</p>
<blockquote>
<p>An iterable object is an object that implements <code>__iter__</code>, which is expected to return an iterator object.</p>
<p>一个可迭代对象是实现了 <code>__iter__</code> 方法的对象,该方法应当返回一个迭代器对象。</p>
<p>-- Python Wiki</p>
</blockquote>
<p>这两年我还写了不少 Rust。在 Rust 里,Python 的 <code>Iterator</code> 对应 <code>std::iter::Iterator</code>,<code>Iterable</code> 则对应 <code>std::iter::IntoIterator</code>。正是有了写 Rust 的经验,我才能一眼看出下面代码的问题:</p>
<pre><code class="language-python">def stream(
      self,
      *,
      # ...
      tools: Iterable | NotGiven = NOT_GIVEN,
      # ...
    ) -&gt; AsyncChatCompletionStreamManager:
    # ...
    _validate_input_tools(tools)
    # ...
    api_request = self.create(
            ...
            tools=tools,
            ...
      )
      return AsyncChatCompletionStreamManager(
            ...
            input_tools=tools,
      )
</code></pre>
<p>这段代码节选自 OpenAI 的 Python 接口 completions.py。问题出现在 #1129 这个 PR。</p>
<p>作为 Rust 选手的直觉告诉我,一个 Iterable 在这里被用到了三次,这就要求这个 Iterable 在多次迭代时本身状态不可变,否则无法保证三次迭代的结果一致。</p>
<p>通俗点说:这个 Iterable 必须保证多次调用 <code>__iter__</code> 返回的迭代器,其迭代结果是一致的。</p>
<p>验证一下。对于普通容器当然没问题,但对于生成器就大有问题。生成器作为迭代器,其状态是会变的,所以三次调用的结果无法保证一致。</p>
<pre><code class="language-python">from typing import Iterable, List

def generate_from_list(lst: List):
    for item in lst:
      yield item

# 容器可以多次迭代,产生相同结果
iterable0 =
assert list(iterable0) == list(iterable0) ==

# 生成器迭代器只能迭代一次
iterable1 = generate_from_list()
assert list(iterable1) ==
assert list(iterable1) == []

# 容器外套一层map并不能保持容器可以多次迭代的特征,只能迭代一次
iterable2 = map(lambda x: x + 1, )
assert list(iterable2) ==
assert list(iterable2) == []

# 生成器额迭代器外套一层map自不必多说,只能迭代一次
iterable3 = map(lambda x: x + 1, generate_from_list())
assert list(iterable3) ==
assert list(iterable3) == []
</code></pre>
<p>运行结果:</p>
<p>godbolt</p>
<h2 id="反思">反思</h2>
<p>我不反思,我一眼就看出这个问题了,我为啥要反思。但需要反思的人还真不少。</p>
<p>写代码时一定要清楚,自己进行的每一个操作对前置操作有什么依赖,是否修改了某个状态,这个状态是否应该由这个操作修改。如果不确定,就必须按照最保守的策略来。</p>
<p>还是以 <code>Iterable</code> 为例,拿到一个可迭代对象后,如果不确定它是不是容器、能否多次迭代且结果一致,那就只允许自己对它做一次迭代。如果因为种种原因不得不多次迭代,那就把它转成 <code>list</code>,再在这个 <code>list</code> 上反复迭代。</p>
<p>#1129 的错误略有不同,它不是在写新代码时出的问题,而是在放宽已有代码的限制时,只考虑了容器的情况,没考虑到其他典型可迭代对象(生成器、map 等)。这个 PR 合并一年多了,似乎还没人踩坑,这恰恰说明大多数场景下传入的就是容器(List),原 PR 放宽约束意义不大,反而引入了潜在风险。</p>
<p>#1606 的错误则更直接。一个 <code>validate_input_tools</code> 函数本不该对输入数据做任何更改,但根据刚才的讨论,这里有潜在的对 <code>Iterable</code> 状态的修改,所以这个函数的类型标注就应该只接受容器,或者其他能表示<strong>不可变</strong>可迭代对象的类型。</p>
<h2 id="总结">总结</h2>
<p>总之,<code>Iterable</code> 虽然是 Python 类型标注中常见的一个概念,但它的“可多次迭代且结果一致”这一点很容易被忽视。写代码时,尤其是在类型标注、接口设计和数据处理时,一定要明确区分“容器型可迭代对象”和“一次性可迭代对象”,避免踩坑。遇到不确定的情况,最保险的做法就是把它转成 <code>list</code>,这样既保证了多次迭代的一致性,也让代码的行为更加可控和易于理解。</p>
<p>(🤔感觉这个还是不要做面试题了)</p><br><br>
来源:https://www.cnblogs.com/connection-aborted/p/18988900
頁: [1]
查看完整版本: Iterable:一个容易被忽视的Python编码细节