Python计数循环执行的惯用语法

6
如果循环遍历一个列表/元组/序列,你可以使用len(...)来推断循环执行的次数。但是当循环遍历迭代器时,你就不能这样做了。
[更新以便更清晰:我在考虑单次使用有限迭代器,在这种情况下我想要同时对项目进行计算和计数。]
我目前使用显式计数器变量,如下面的例子所示:
def some_function(some_argument):
    pass


some_iterator = iter("Hello world")

count = 0
for value in some_iterator:
    some_function(value)
    count += 1

print("Looped %i times" % count)

考虑到 "Hello world" 中有11个字符,期望输出应该是:

Looped 11 times

我也考虑过使用 enumerate(...) 这种更简短的方式,但我认为这并不是很清晰:

def some_function(some_argument):
    pass


some_iterator = iter("Hello world")

count = 0  # Added for special case, see note below
for count, value in enumerate(some_iterator, start=1):
    some_function(value)

print("Looped %i times" % count)

[参考更新:@mata指出,如果最初的代码中迭代器为空,则第二个示例将失败。插入count = 0解决了这个问题,或者我们可以使用for ... else ...结构来处理这种情况。]
它不使用enumerate(...)中的索引,而是将变量设置为循环计数几乎是一个副作用。对我来说,这非常不清楚,因此我更喜欢具有显式增量的第一个版本。
是否存在一种被接受的Pythonic方式来实现这一点(最好适用于Python 3和Python 2代码)?

是的,这个解决方案是“enumerate”。为什么您认为它不够“清晰”?我不知道是否只是需要习惯,但对我来说,“enumerate”似乎易读且简洁... - user2390182
3
这要看情况……我认为第二种形式更符合Python的风格,但它并不涵盖完全相同的情况,因为如果迭代器没有元素,则循环后count未定义,并且会出现NameError,所以你必须使用 for ... else 或在循环之前初始化计数器。 - mata
很好的观点@mata,如果迭代器为空,则按照第二个示例编写的代码将会失败。 - Peter Cock
4个回答

6
您可以通过添加一行代码将enumerate的方便性与在循环未运行时定义计数器相结合:
count = 0  # Counter is set in any case.
for count, item in enumerate(data, start=1):
   doSomethingTo(item)
print "Did it %d times" % count

如果您只需要计算迭代器中的项目数量,而不对项目进行任何处理或制作列表,则可以简单地执行以下操作:

count = sum(1 for ignored_item in data)  # count a 1 for each item

1
这段代码存在一个 off by one 错误(默认情况下,“enumerate”使用“0”到“count - 1”),这就是为什么我需要 verbose 额外参数从一开始的原因。 - Peter Cock
我熟悉使用 sum 函数的列表压缩技巧,但在这种情况下,我需要对项目执行某个操作(在示例中用“some_function”表示)。 - Peter Cock
1
@peterjc:很好的发现,谢谢!我已经用 start=1 更新了我的答案。 - 9000

2

您可以运用各种方法来计算生成器中项目的数量,但无论如何,原始生成器都将被耗尽。确切地说,它已经被“消耗”了。

length = sum(1 for x in gen)
length = max(c for c, _ in enumerate(gen, 1))
length = len(list(gen))
  1. 这里展示的第一种方法非常好,因为它能很好地处理空生成器的情况,并按预期返回零。
  2. 当给定一个耗尽的生成器时,第二个代码将引发异常,这可能在它永远不应该被耗尽的情况下非常有用,但实际上它确实被耗尽了,所以执行将停止,您将能够调查出错的原因。
  3. 如果gen提供的数据太大,这段代码可能会浪费很多内存,但它很容易理解,因此没有人需要费力思考并试图理解这意味着什么。

所有这些都仅适用于有限的生成器。

如果您想在循环迭代器时计算迭代器的“长度”,可以这样做:

length = 0
for length, data in enumerate(gen, 1):
    # do stuff

现在,length将等于生成器产生的元素数量。请注意,您不必手动增加length,因为循环执行后lengthdata仍然可用且有效。

编辑:如果您想对每个值执行某些函数并忽略其返回值(您可以通过使用列表作为函数参数之一来处理它),可以尝试以下操作:

length = sum(1 | bool(function(x)) for x in gen)

这将在对生成器的每个元素应用function的同时计算长度。然而,使用enumerate似乎是更好的选择。

你提供的所有示例都不允许使用生成器中的项目来运行代码(这可能是一些昂贵的计算或只使用一次的计算),但你最终推荐使用“enumerate”方法。 - Peter Cock
@peterjc,我已经添加了一个示例,演示了如何将函数应用程序合并到我的初始解决方案中。 - ForceBru
这是一种创新的生成器表达式方法,将计数与对每个项目调用函数相结合,但就口味而言,我觉得它太复杂了。我的问题的性质意味着评判替代方案总是主观的。 - Peter Cock

1

无法获取迭代器中项目的数量。考虑这种情况

def gen():
   a = 1
   while True:
       yield a
       a += 1
f = gen()

for value in f:
    # do something

这个迭代器的大小是多少?当迭代器引发 StopIteration 时,迭代器就结束了。相反,当你遍历一个序列时,序列已经存在,因此它的长度可以知道。
你使用的两种方法都可以。最好的方法取决于你的口味。另一种选择是使用...
from itertools import count
for item, counter in zip(iterator, count()):
    # do stuff

然而,在大多数情况下,我认为您的第一种传统方法会更清晰。


这是一个很好的例子,可以解释为什么我会问这个问题 - 在介绍中,我只是简单地提到了不能在迭代器上使用“len(...)”。但是你的答案并没有回答我的问题,即在使用for循环时,如何最好地计算迭代器中的项目数量。 - Peter Cock
@peterjc:好的,只是想指出这是不可能的。已经编辑了问题。 - blue_note
谢谢 - 现在更清楚了。我同意“最佳”方法将是品味的问题。 - Peter Cock

0

这有什么问题吗?

len(list(some_iterator))

3
它确实可行,但会将some_iterator的所有元素读入内存。 - Daniel Hepper

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