Python仅在for循环未开始迭代(使用生成器)时执行代码?

10
for/else子句中,如果迭代完成但未被break中断,则执行else块,所以我阅读了
是否有一种语言结构可以让我编写仅在for循环未开始迭代时执行的内容?如果我正在使用tuplelist,我会这样做:
if seq:
    for x in seq:
         # something
else:
    # something else

但是当我使用生成器时,我得不到我想要的行为:

>>> g = (x for x in range(2))
>>> for x in g:
...     print x
... else:
...     print "done"
... 
0
1
done    # I don't want "done" here
>>> g = (x for x in range(2) if x > 1)
>>> if g:
...     for x in g:
...         print x
... else:
...     print "done"
... 
>>>     # I was expecting "done" here

我该如何在使用for循环的情况下,避免从生成器中创建一个tuplelist,同时完成操作而不会过度消耗资源?我可以在while循环中使用next()并尝试捕获StopIteration,但我想知道是否有更好的方法可以使用for实现。

1
我可能会在循环内设置一个 ran 标志,并使用 if not ran: - user2357112
1
你不能这样做。请参见https://dev59.com/uHRB5IYBdhLWcg3wUVtd - Ludo
@Ludo 我知道我无法事先确定生成器是否为空。我只是想知道是否有一种好的语言结构来处理这种情况。 - 2rs2ts
1
好问题。我也是为了同样的事情来这里的。我已经写了超过7年的Python,只是想知道是否有一些隐藏在语言中的语法糖我从未发现过。可惜的是,并没有。 - John Carrell
8个回答

7

在for循环中更新布尔值是我所能想到的最好方法。

any_results = False
for x in g:
    any_results = True
    print x
if not any_results:
    print 'Done'

2
是的,我通常会这样做;我只是想知道是否有控制结构或其他语言构造可以简洁地完成这个任务。 - 2rs2ts
这就像拥有一个计数器一样。我们不能使用 Python 提供的 for...else 吗? - ns15
@shadow0359 - 我不明白,一个空的 g 上的 for...else 怎么会以 true 退出。 - Peter DeGlopper

6
n = -1
for n, i in enumerate(it):
    do_stuff()
if n < 0:
    print 'Done'

我喜欢使用enumerate - kindall

5

我发现这个解决方案更好。点击此链接获取更多信息(http://python-notes.curiousefficiency.org/en/latest/python_concepts/break_else.html)。

你可以使用自定义的哨兵值: x = no_data = object()

x = no_data = object()
for x in data:
    .......
if x is no_data:
    print "Loop did not run"

object()返回一个无特征的对象,它是所有类的基类。

is检查两个对象是否相同(x is no_data)。如果它们保持相同,则意味着控制从未进入for循环。


我认为当x = no_data时,两个指向生成器的点可能存在一些风险,因为生成器在for循环后会耗尽。 - Menglong Li
1
起初,xno_data都指向一个object实例。data是生成器,从中x获取值。如果循环运行,则x将发生变化,并且x is no_data肯定为False。在我看来,这没有风险。 - Stam Kaly
@孟龙,你所指的风险是什么? - ns15
我认为这是for循环最干净的解决方案。在我看来,将哨兵对象称为“no_data_sentinel”会使代码更清晰。 - pabouk - Ukraine stay strong

2
您可以使用生成器函数: next接受一个可选的第二个参数,用于在迭代器耗尽时指定默认值。
def func(lis):
    g = (x for x in lis if x > 1)
    flag = object()      # expected to be unique
    nex = next(g, flag)  # will return flag if genexp is empty
    if nex is not flag:
        yield nex
        for item in g:
            yield item
    else:
        yield "done"

for x in func(range(2)):
    print x
print "------"
for x in func(range(4)):
    print x

输出:

done
------
2
3

这种方法在效率上与布尔方法或执行“tuple(g)”相比如何?当然,还要考虑内存和执行时间。 - 2rs2ts
由于迭代器具有一些额外的开销,所以与布尔方法相比,它们会变慢,但优点是您可以使用itertools.islice访问有限数量的项目(即切片),而无需遍历所有项目(如果在任何情况下需要),您不能使用简单的for循环完成。阅读此处以比较元组/列表和genexp之间的差异。 - Ashwini Chaudhary
这真的非常酷。现在我会更经常地使用列表推导式来处理较小的数据集。无论如何,对于我的目的,由于我不需要任何切片或其他形式的迭代,我将采用布尔方法。但是,在您的答案和评论之间,这非常有用。谢谢! - 2rs2ts

0
我认为了解循环是否实际执行的好方法是使用循环变量。
lv= 1
for x in g:
    lv = lv+1
    print x
if (lv == 1):
    print 'Done'

我的语法可能有误,因为我不是 Python 的专家。


0
你可以编写一个包装器来计算迭代次数。它的优点是可以与更奇特的枚举一起使用。在Python3中,它可能是这样的:
import sys
from glob import iglob

class GenCount(object):

    def __init__(self, gen):
        self._iter = iter(gen)
        self.count = 0

    def __next__(self):
        val = self._iter.__next__()
        self.count += 1
        return val

    def __iter__(self):
       return self

c = GenCount(iglob(sys.argv[1]))
for fn in c:
    print(fn)
print(c.count)


c = GenCount(iglob(sys.argv[1]))
print([fn for fn in c])
print(c.count)

0
您可以检查 x 是否在循环中定义。
for x in (y for y in range(2) if y > 1):
    print(x)

try:
    print(f'do something with {x}')
except NameError:
    print('the loop did not run')

然而,在循环之前,请确保x未被定义。


-1
在这个例子中,你需要额外的结构吗?
caught = None
for item in x:
    caught = item
    print caught
if caught != None: print "done"

*编辑了OP的评论*


不,我希望它只在没有任何可迭代的内容时打印“done”。 - 2rs2ts

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