如何限制循环的迭代次数?

85

假设我有一个项目列表,并且我想迭代其中的前几个:

items = list(range(10)) # I mean this to represent any kind of iterable.
limit = 5

朴素实现

来自其他编程语言的Python新手可能会编写下面这段代码,虽然不太符合惯用法但是完全可行且具有良好性能:

index = 0
for item in items: # Python's `for` loop is a for-each.
    print(item)    # or whatever function of that item.
    index += 1
    if index == limit:
        break

更加惯用的实现方式

但 Python 有 enumerate 函数,可以将其中一半的代码优雅地简化:

for index, item in enumerate(items):
    print(item)
    if index == limit: # There's gotta be a better way.
        break

所以我们已经将额外的代码减少了一半。但一定有更好的方法。

我们能否近似下面的伪代码行为?

如果枚举函数增加另一个可选参数stop(例如,它像这样带有start参数:enumerate(items, start=1)),我认为那将是理想的,但是下面的内容不存在(请参见此处枚举函数的文档):

# hypothetical code, not implemented:
for _, item in enumerate(items, start=0, stop=limit): # `stop` not implemented
    print(item)

注意,不需要给index命名,因为没有必要引用它。

有没有惯用的方式来写上面的内容?如何写?

第二个问题:为什么这不是内置在enumerate中的?

6个回答

146

How can I limit iterations of a loop in Python?

for index, item in enumerate(items):
    print(item)
    if index == limit:
        break

Is there a shorter, idiomatic way to write the above? How?

包括索引

zip 将停留在其参数中最短的可迭代对象。(与使用最长可迭代对象的 zip_longest 的行为相反。)

range 可以提供一个有限的可迭代对象,我们可以将其与主要的可迭代对象一起传递给 zip

因此,我们可以将一个 range 对象(带有其 stop 参数)传递给 zip 并像使用有限枚举器一样使用它。

zip(range(limit), items)

使用 Python 3,ziprange 返回可迭代对象,这些对象将数据流式传输,而不是将数据材料化为列表进行中间步骤。

for index, item in zip(range(limit), items):
    print(index, item)

要在Python 2中获得相同的行为,只需将range替换为xrange,将zip替换为itertools.izip
from itertools import izip
for index, item in izip(xrange(limit), items):
    print(item)

如果不需要索引,可以使用itertools.islice

您可以使用itertools.islice

for item in itertools.islice(items, 0, stop):
    print(item)

这不需要分配索引。

使用enumerate(islice(items, stop))组合以获取索引

正如Pablo Ruiz Ruiz所指出的那样,我们也可以将islice与enumerate组合使用。

for index, item in enumerate(islice(items, limit)):
    print(index, item)

为什么这不是内置于enumerate中的呢? 这里是纯Python实现的枚举(可能需要在注释中进行修改以获得所需行为):
def enumerate(collection, start=0):  # could add stop=None
    i = start
    it = iter(collection)
    while 1:                         # could modify to `while i != stop:`
        yield (i, next(it))
        i += 1

上面的代码对那些已经使用枚举的人来说性能会更低,因为它需要在每次迭代时检查是否到达停止时间。我们可以只进行检查并在没有停止参数的情况下使用旧的枚举。
_enumerate = enumerate

def enumerate(collection, start=0, stop=None):
    if stop is not None:
        return zip(range(start, stop), collection)
    return _enumerate(collection, start)

这个额外的检查会对性能产生轻微可忽略的影响。
至于为什么枚举没有停止参数,这最初是提出来的(参见PEP 279):

This function was originally proposed with optional start and stop arguments. GvR [Guido van Rossum] pointed out that the function call enumerate(seqn, 4, 6) had an alternate, plausible interpretation as a slice that would return the fourth and fifth elements of the sequence. To avoid the ambiguity, the optional arguments were dropped even though it meant losing flexibility as a loop counter. That flexibility was most important for the common case of counting from one, as in:

for linenum, line in enumerate(source,1):  print linenum, line
显然,“start”被保留是因为它非常有价值,“stop”被删除是因为它的使用情况较少,会导致新函数的使用混乱。

避免使用下标符号进行切片

另一个答案说:

Why not simply use

for item in items[:limit]: # or limit+1, depends
以下是一些缺点:
  • 它仅适用于可迭代对象,而且必须支持切片,因此它的使用范围更有限。
  • 如果它们支持切片,通常会在内存中创建一个新的数据结构,而不是在引用数据结构上进行迭代,因此会浪费内存(所有内置对象在切片时都会复制,但例如numpy数组在切片时会创建一个视图)。
  • 不可切片的可迭代对象需要另一种处理方式。如果您切换到惰性评估模型,则还必须更改带有切片代码的代码。

只有在您了解其限制以及它是复制还是视图时,才应使用下标符号进行切片。

结论

我认为现在Python社区知道了enumerate的用法,混淆成本将被价值所超越。

在那之前,您可以使用:

for index, element in zip(range(limit), items):
    ...

或者

for index, item in enumerate(islice(items, limit)):
    ...

或者,如果您根本不需要索引:
for element in islice(items, 0, limit):
    ...

避免使用下标符号进行切片,除非您了解其限制。

1
如果您完全不需要索引,并且想从开头开始,就不需要添加Start参数,只需使用以下代码: for element in islice(items, limit): - Walter R

33
您可以使用itertools.islice来实现。如果您只传递一个参数,则该参数将被视为stop。它接受startstopstep参数,并可与任何可迭代对象一起使用。
itertools.islice(iterable, stop)
itertools.islice(iterable, start, stop[, step])

演示:

>>> from itertools import islice
>>> items = list(range(10))
>>> limit = 5
>>> for item in islice(items, limit):
    print item,
...
0 1 2 3 4

文档中的示例:

islice('ABCDEFG', 2) --> A B
islice('ABCDEFG', 2, 4) --> C D
islice('ABCDEFG', 2, None) --> C D E F G
islice('ABCDEFG', 0, None, 2) --> A C E G

这是限制生成器迭代次数最优雅的方式!此外,您可以指定 islice(generator, None) 来获取所有迭代 - 如果您想根据某些条件限制迭代次数,则这非常有用。 - Maksym Ganenko
谢谢您上面的回答。这正是我需要的,可以帮助我调试长时间运行的进程,以便我可以深入输出列表并解决代码中的错误。这应该是被接受的答案。 - S. J.

31

为什么不直接使用呢?

for item in items[:limit]: # or limit+1, depends
    print(item)    # or whatever function of that item.

这将仅适用于某些可迭代对象,但由于您指定了列表,因此它可以正常工作。

如果使用集合或字典等其他对象,则无法使用。


5
是的,列表、元组和范围对象可以进行切片,但它忽略了很多东西,比如迭代器、集合和生成器。另一个缺点是它会创建一个新的数据结构,例如列表或元组,而不是懒惰地评估数据。 - Russia Must Remove Putin

1
为什么不像这样循环,直到达到限制或列表结束,以先发生的为准:

items = range(10)
limit = 5
for i in range(min(limit, len(items))):
  print items[i]

输出:

0
1
2
3
4

1

使用enumerate函数将islice函数中的限制传入

a = [2,3,4,2,1,4]

for a, v in enumerate(islice(a, 3)): 
   print(a, v)

输出:

0 2
1 3
2 4

-2

简短解决方案

items = range(10)
limit = 5

for i in items[:limit]: print(i)

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