Python的itertools.repeat的目的是什么?

35

对于我能想到的Python itertools.repeat() 类的每个用途,我都可以想到另一个同样(可能更)可接受的解决方案来实现相同的效果。例如:

>>> [i for i in itertools.repeat('example', 5)]
['example', 'example', 'example', 'example', 'example']
>>> ['example'] * 5
['example', 'example', 'example', 'example', 'example']

>>> list(map(str.upper, itertools.repeat('example', 5)))
['EXAMPLE', 'EXAMPLE', 'EXAMPLE', 'EXAMPLE', 'EXAMPLE']
>>> ['example'.upper()] * 5
['EXAMPLE', 'EXAMPLE', 'EXAMPLE', 'EXAMPLE', 'EXAMPLE']

itertools.repeat()在什么情况下会是最合适的解决方案?如果有的话,是在什么情况下使用?


5
我添加了一个新答案,展示了itertools repeat的最初动机用例。此外,我刚刚更新了Python文档以反映这个使用说明。 - Raymond Hettinger
你的四个代码示例中有三个实际上是无法工作的。第一个创建了一个生成器表达式,而不是一个元组(你需要使用 tuple(itertools.repeat('example', 5))),第二个将 'example' 本身乘以五次以创建 'exampleexampleexampleexampleexample',因为 ('example') 本身并没有创建一个元组(你需要使用 ('example',) * 5),而你的第三个示例使用了 map,这会返回一个 map 对象,因为 Python 3 中的 map 是惰性的(你需要将其包装在 list 中才能得到提供的结果)。这是一个有趣的问题,但是伪造你的代码示例会损害它的可信度。 - ShadowRanger
@ShadowRanger,我在发布这篇文章时对Python还很陌生,只是快速打了一些示例而没有检查实际输出。有点小题大做,但我现在已经修复了。谢谢! :) - Tyler Crompton
6个回答

46

itertools.repeat 的主要目的是提供一系列不变的值,以供 mapzip 使用:

>>> list(map(pow, range(10), repeat(2)))     # list of squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

其次,它提供了一种非常快速的循环固定次数的方法,如下所示:

for _ in itertools.repeat(None, 10000):
    do_something()

这比以下方式更快:

for i in range(10000):
    do_something().
前者胜出是因为它只需更新现有None对象的引用计数。后者失败是因为range()xrange()需要创建10000个不同的整数对象。
注意,Guido本人在timeit()模块中使用了这种快速循环技巧。请参见https://hg.python.org/cpython/file/2.7/Lib/timeit.py#l195源代码:
    if itertools:
        it = itertools.repeat(None, number)
    else:
        it = [None] * number
    gcold = gc.isenabled()
    gc.disable()
    try:
        timing = self.inner(it, self.timer)

3
这个答案和 repeat 真是太棒了。为什么它被隐藏在 itertools 中而不是内置函数中呢?for _ in range(x): do() 是一个如此常见的模式。 - Darkonaut
@Veky 你似乎把 Python 这门语言和它的一个实现(CPython)混淆了。Raymond 提到的引用计数不是语言的一部分。语言本身没有速度的概念。 - Darkonaut
我不明白你为什么会这样想。但我可以向你保证,当Guido设计Python时,他设计了一个非常具体的实现。(是的,CPython。其他实现几乎是在10年后出现的。)而在那种语言中,循环n次的Pythonic方式是使用range,而不是repeat。即使range返回一个列表,它也是首选(xrange稍后才出现)。因此,显然速度不是主要问题。我根本没有提到引用计数。 - Veky
2
@Veky 我的假设不是“Python被设计成快速的”,也不必意味着只提供比for _ in range(x): do()更好的习惯用语,对于我不关心序列的情况,但对于紧密循环有些更快的东西会很好。我的最初评论是关于解释器和库的,你的评论则是关于语言设计,并使其听起来好像使用除range()之外的任何东西都会为速度牺牲可读性。Python的可读性主要源于语法,而不是与解释器一起提供哪些函数或它们的实现方式。 - Darkonaut
@Veky 无论如何,我认为在高级语言中,在可读性和性能之间没有太多的目标冲突,除了一次性决策,对于不允许对内存使用进行细粒度控制的语言。对于CPython解释器,性能始终是一个关注点。例如,您会发现每个CPython更新都有与性能相关的改进(例如Python 3.9中的地板除法)。值得注意的是,Guido反对放弃GIL,只要它会导致单线程执行的性能下降! - Darkonaut
显示剩余2条评论

32

itertools.repeat函数是惰性的;它只使用一个条目所需的内存。另一方面,(a,) * n[a] * n习语在内存中创建n个对象的副本。对于五个项目,乘法习语可能更好,但如果您需要重复某些东西,比如说一百万次,您可能会注意到资源问题。

尽管如此,很难想象有许多静态用途可以使用itertools.repeat。但是,itertools.repeat是一个函数,这使您可以将其用于许多函数应用程序。例如,您可能有一些库函数func,该函数操作输入的可迭代对象。有时,您可能具有预先构建的各种项目列表。其他时候,您可能只想操作一个统一的列表。如果列表很大,itertools.repeat将为您节省内存。

最后,repeat使得所谓的“迭代器代数”成为可能,该功能在itertools文档中进行了描述。即使itertools模块本身也使用repeat函数。例如,下面的代码被给定为itertools.izip_longest的等效实现(即使实际代码可能是用C编写的)。请注意,从底部数第七行使用了repeat

class ZipExhausted(Exception):
    pass

def izip_longest(*args, **kwds):
    # izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
    fillvalue = kwds.get('fillvalue')
    counter = [len(args) - 1]
    def sentinel():
        if not counter[0]:
            raise ZipExhausted
        counter[0] -= 1
        yield fillvalue
    fillers = repeat(fillvalue)
    iterators = [chain(it, sentinel(), fillers) for it in args]
    try:
        while iterators:
            yield tuple(map(next, iterators))
    except ZipExhausted:
        pass

11
小小的疑问:[a] * n 不会在内存中创建n个a的副本,而是创建n个指向单个a副本的引用。在某些情况下,这种差异可能非常显著;尝试一下 a = [[]] * 5; a[0].append(1) - Thomas K
5
好观点。我总是忘记 Python 中几乎所有东西都是引用。我猜这也在某种程度上缓解了内存使用问题,但我猜一百万个引用仍然需要相当多的资源。 - HardlyKnowEm

16

你举的例子foo * 5看起来与itertools.repeat(foo, 5)有些相似,但实际上它们是非常不同的。

如果你写foo * 100000,解释器必须在给出答案之前创建10万个foo的副本。因此,这是一种非常昂贵和不友好的内存操作。

但是如果你写itertools.repeat(foo, 100000),解释器可以返回一个扮演同样功能的迭代器,并且不需要计算结果,直到你需要它 - 比如,在使用它的函数中要求知道序列中的每个结果时。

这就是迭代器的主要优点:它们可以将计算一个部分(或者全部)列表的工作推迟,直到你真正需要答案。


为什么不直接使用 for i in range(100000):,然后在循环内部访问 foo,而不是询问此函数您给了它什么值? - Tyler Crompton
@TylerCrompton:迭代器可以传递给其他期望任何类型迭代器的东西,而不考虑其内部内容。您无法使用范围执行相同操作(它是可迭代的,但本身不是迭代器)。 - John Feminella
我理解你的观点,但是就你评论结尾所说的Python 3而言? - Tyler Crompton
1
range 在 Python 3 中是一个迭代器,但在 Python 2 中它返回一个列表。在 Python 2 中使用 xrange 来获得迭代器;在 Python 3 中使用 list(range(...)) 来获得列表。 - HardlyKnowEm
抱歉,我没有看到这个问题被标记为Python-3。是的,@mlefavor是正确的。 - John Feminella
@HardlyKnowEm:迂腐的说法:Py3中的range和Py2中的xrange是惰性的,但它们本身并不是迭代器。它们是可迭代对象,而不是迭代器。它们是不可变序列(在Python 2 xrange上略有缺陷,在Python 3上则相当完整),只是在需要时计算其内容。当您两次迭代同一个对象时,这会产生差异;r = range(10)(在Py2上为xrange),然后是sum(r),再然后是sum(r),每次都会产生相同的结果;如果它是迭代器,则第二个调用将产生0(因为第一个调用将耗尽迭代器)。 - ShadowRanger

3

这是一个迭代器,重要提示:它在itertools模块中。从您提供的文档中可以看到:

itertools.repeat(object[, times]) 创建一个迭代器,该迭代器会一遍又一遍地返回对象。除非指定了times参数,否则会无限循环。

因此,您永远不会将所有内容保存在内存中。您可能需要使用它的一个示例是:

n = 25
t = 0
for x in itertools.repeat(4):
    if t > n:
        print t
    else:
        t += x

这样做可以让你拥有任意数量的4,或者无限量的其他数字列表。


3
你可以将第3行改为 while True:,将第7行的 x 改为 4,这将达到完全相同的效果,更易读,并且速度略有提升。这就是我想知道它是否有任何目的的原因。 - Tyler Crompton
1
注意:有趣的是,在Python 2中,'while True:'比'for x in itertools.repeat(4):'慢,因为'True'当时不是关键字,所以在每次循环中实际上加载并测试它是否为真以确保没有人重新分配它('while 1:'是真正的无条件无限循环)。'repeat'将迭代器保留在堆栈上(不需要在内置范围内查找),从而节省了这些工作。值得庆幸的是,在Python 3中,'True'和'False'是关键字,因此'while True:'在字节码级别上真正地成为了无条件的无限循环。 - ShadowRanger

3

如前所述,它与 zip 配合良好:

另一个示例:

from itertools import repeat

fruits = ['apples', 'oranges', 'bananas']

# Initialize inventory to zero for each fruit type.
inventory = dict( zip(fruits, repeat(0)) )

结果:

{'apples': 0, 'oranges': 0, 'bananas': 0}

为了避免重复,我需要涉及到 len(fruits)

3
inventory = {fruit: 0 for fruit in fruits}这段代码更易读且稍微快一些。 - Tyler Crompton
@TylerCrompton 确实。我不确定我以前是否使用过该语法来初始化字典。或者我只是使用了太多的LINQ :-) 感谢您提供有用的评论。 - Jonathon Reinhart
1
@TylerCrompton:如果我们追求速度,dict.fromkeys(fruits, 0) 是最快的(由于稍微更高的固定开销,对于只有三个具有恒定值的项不是最佳选择,但随着fruits中项目数量的增加, dict.fromkeys 开始领先,大约从八个项目开始); 在我的机器上渐进地运行比用于巨大输入的dict理解快约2/3。自3.6以来(对于dict有保证的排序),dict.fromkeys(x)是一个真正高效的方式来保持顺序的唯一化输入(不像set(x),它会丢失顺序)。 - ShadowRanger

0

我通常会将repeat与chain和cycle一起使用。以下是一个例子:

from itertools import chain,repeat,cycle

fruits = ['apples', 'oranges', 'bananas', 'pineapples','grapes',"berries"]

inventory = list(zip(fruits, chain(repeat(10,2),cycle(range(1,3)))))

print inventory

将前两个水果的值设为10,然后对剩下的水果循环使用值1和2。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接