表示可以多次迭代的 Iterable 的 Pythonic 方式是什么

Car*_*ten 8 python api-design type-hinting

我想就最pythonic的方式获得您的建议,以使用类型提示在python中表达以下函数:

我想公开一个函数作为接受输入参数并返回输出的库的一部分。输入参数的契约应该是:

  • 我的函数可以迭代它
  • 如果我的函数维护对输入的引用(例如通过返回一个保留该引用的对象),那没关系
  • 可以多次迭代输入

一个例子可能是一个函数,它接受一系列 URL,然后向这些 URL 发出请求,可能带有一些重试逻辑,所以我必须不止一次迭代原始序列。但我的问题不仅仅是这个样本。

乍一看,合适的签名是:

from typing import Iterable

def do_sth(input: Iterable[str]) -> SomeResult:
  ...
Run Code Online (Sandbox Code Playgroud)

然而,这违反了第三个要求,因为在 python 中不能保证您可以多次迭代一个 Iterable,例如因为迭代器和生成器本身就是可迭代的。

另一种尝试可能是:

from typing import Sequence

def do_sth(input: Sequence[str]) -> SomeResult:
  ...
Run Code Online (Sandbox Code Playgroud)

但是Sequence合同超出了我的功能要求,因为它包括索引访问和长度知识。

我想到的一个解决方案是使用Iterable签名,然后在内部制作输入的副本。但是如果源序列很大,这似乎会引入潜在的内存问题。

有没有解决方案,即python是否知道Iterable每次都会返回一个新迭代器的概念?

Rec*_*nic 6

我能想到有两种自然的方式来表示这一点。

第一种是使用Iterable[str], 并在文档中提到,不应使用IteratorGenerator对象,因为您可能会多次调用__iter__. 重点Iterable是您可以在其上获得迭代器,可以说,一开始提供Iterator支持就是一个错误。Iterable它并不完美,但很简单,通常比技术上更正确但非常复杂的注释更“Pythonic”。

您可以添加一些运行时检查,如果用户传递了错误的内容,则会提醒用户存在问题:

iter1 = iter(input)
for item in iter1:
    do_something(item)
iter2 = iter(input)
if iter2 is iter1:
    raise ValueError("Must pass an iterable that can be iterated multiple times. Got {input}.")
Run Code Online (Sandbox Code Playgroud)

或者检查你是否有迭代器,并用内存惩罚来处理它:

if isinstance(input, Iterator):
    input = list(input)  # or itertools.tee or whatever
    warn("This may eat up a lot of memory")
Run Code Online (Sandbox Code Playgroud)

另一种选择是使用io.TextIOBase. 这可以通过查找开头来迭代多次。这取决于您的用例,并且可能不太适合。如果从概念上讲,输入是字符序列上的某种分块视图,那么 io 流就很适合,即使迭代器在技术上不返回文本行。如果从概念上讲它是不连续的字符串序列,那么流就不适合。