尝试对空迭代进行循环的惯用方式的解决方法

6
假设我正在遍历一个可迭代对象,并且希望在迭代器为空时执行某些操作。我能想到的最好的两种方法是:
- 使用内置函数 `next()` 获取可迭代对象的首个元素并进行判断,如果没有元素则执行相应的操作。 - 将可迭代对象转换为列表,然后检查列表是否为空,如果为空则执行相应的操作。
请注意,这两种方法都有其优缺点,具体取决于您的要求和情况。
for i in iterable:
     # do_something
if not iterable:
    # do_something_else

并且

empty = True
for i in iterable:
    empty = False
    # do_something
if empty:
    # do_something_else

第一个方法依赖于可迭代对象是一个集合(因此当可迭代对象被传递到函数/方法中并在其中进行循环时无用),而第二个方法在每次循环通过设置“empty”来实现,这似乎很丑陋。
我是否漏掉了其他方法,还是第二种替代方案最好?如果有一些子句可以添加到循环语句中来处理这个问题,就像“else”使“not_found”标志消失一样,那将非常酷。
我不想要聪明的技巧。
我不想要涉及大量代码的解决方案。
我正在寻找一种简单的语言功能。我正在寻找一种清晰和Pythonic的方法来迭代可迭代对象,并在可迭代对象为空时执行某些操作,任何有经验的Python程序员都会理解。如果我可以在每次迭代时不设置标志就能实现,那就太好了。如果没有简单的习语可以做到这一点,那就算了。

当然,那并不是很重要,但我认为你的例子中应该这样注释: a <= x < b - Bolo
另请参阅:https://dev59.com/uHRB5IYBdhLWcg3wUVtd 和 https://dev59.com/zHI-5IYBdhLWcg3wNle1 - Manoj Govindan
8个回答

3

这个做法比较巧妙,但是你可以删除i,然后在循环后检查它是否存在(如果不存在,则循环从未发生):

try:
    del i
except NameException: pass

for i in iterable:
    do_something(i)

try:
    del i
except NameException:
    do_something_else()

我认为这可能比使用标志更难看。

1
这很聪明,但正如你在最后一句话中所预料的那样,它并不完全符合我的要求。话虽如此,聪明总是值得加一分的。 - aaronasterling

3

我认为这是最干净的方法:

# first try with exceptions
def nonempty( iter ):
    """ returns `iter` if iter is not empty, else raises TypeError """
    try:
        first = next(iter)
    except StopIteration:
        raise TypeError("Emtpy Iterator")
    yield first
    for item in iter:
        yield item


# a version without exceptions. Seems nicer:
def isempty( iter ):
    """ returns `(True, ())` if `iter` if is empty else `(False, iter)`
         Don't use the original iterator! """
    try:
        first = next(iter)
    except StopIteration:
        return True, ()
    else:
        def iterator():
            yield first
            for item in iter:
                yield item
        return False, iterator()



for x in ([],[1]):
    # first version
    try:
        list(nonempty(iter(x))) # trying to consume a empty iterator raises
    except TypeError:
        print x, "is empty"
    else:
        print x, "is not empty"

    # with isempty
    empty, it = isempty(iter(x))
    print x,  "is", ("empty" if empty else "not empty")

+1。那是迄今为止最好的答案。我可能会接受这个答案。 - aaronasterling

2

更新2

我喜欢Odomontois的回答。在我看来,它比我下面写的更适合这个问题。

更新

(在阅读了原帖的评论和编辑后) 你也可以这样做。见下文:

def with_divisible(n, a, b, f):
 it = (i for i in xrange(a, b) if not i % n)
 for i in wrapper(it):
  f(i)

>>> with_divisible(1, 1, 1, lambda x: x)
Traceback (most recent call last):
  File "<pyshell#55>", line 1, in <module>
    with_divisible(1, 1, 1, lambda x: x)
  File "<pyshell#54>", line 3, in with_divisible
    for i in wrapper(it):
  File "<pyshell#46>", line 4, in wrapper
    raise EmptyIterableException("Empty")
EmptyIterableException: Empty

>>> with_divisible(7, 1, 21, lambda x: x)
7
14
...Snipped...
    raise EmptyIterableException("Empty")
EmptyIterableException: Empty

原始回答

有趣的问题。我做了一些实验,得出了以下结论:

class EmptyIterableException(Exception):
    pass

def wrapper(iterable):
    for each in iterable:
        yield each
    raise EmptyIterableException("Empty")

try:
    for each in wrapper(iterable):
        do_something(each)
except EmptyIterableException, e:
    do_something_else()

抱歉,我的问题表述不清楚。我将编辑我的问题。 - aaronasterling
我也把我的编辑搞砸了。虽然这不应该让你困惑。for循环中的那一行是empty = False。无论如何,你的代码都会引发异常。但是,如果我将empty标志移到包装器中,并在包装器内部的if empty处引发异常,则它可以正常工作。对不起,我这么挑剔,但我几乎不能称其为惯用解决方案。尽管如此,我还是点了赞。 - aaronasterling
@aaronasterling:你的表达式返回一个生成器对象。调用next()函数并捕获引发的StopIteration异常会有帮助吗? - Manoj Govindan
我不这么认为 - 我丢失了生成器的第一个值,并且在我编写的这个示例中,它只是一个生成器表达式。我正在尝试找到一种通用的方法来编写像这样的循环。遍历有向图并对终点执行某些操作是另一个示例。 - aaronasterling
@aaronasterling:明白了。另外,如果你正在使用列表,可以使用 if bool(my_list) 来检查列表是否为空。但我不知道如何在生成器中实现这一点。 - Manoj Govindan
你可以使用 if my_list 来检查一个列表是否为空 ;) - aaronasterling

2
if not map(do_something_callable,iterable) : 
    # do something else

那句话怎么能开始回答这个问题呢? - aaronasterling
其实这个方法值得尝试一下。map(f,divisibles(n,a,b)) 函数将会对序列 divisibles 应用函数 f。如果开始时 divisibles 是空的(我相信这是你当初想要测试的),那么 map 的结果将会是一个空列表,因此第二行代码会导致异常。但是如果 divisibles 不为空,map 将会产生 fdivisibles 上的映射值。 - Manoj Govindan
不,这不值得一试。它在我发布的特定示例中表现很好,但对于我所问的更广泛的情况毫无用处。请删除这个答案。 - aaronasterling
但只有当我将“do something”定义为函数时,这才有效。再次强调,我对一般惯用语感兴趣。我很清楚“map”的用途,但它并不总是实用的。 - aaronasterling

1

如果要在迭代器被消耗之前对其进行部分检查,通常的做法是使用 itertools.tee。这样我们就可以拥有两个迭代器副本,检查其中一个是否为空,同时仍然可以从另一个副本开始消耗。

from itertools import tee
it1, it2 = tee(iterable)
try:
    it1.next()
    for i in it2:
        do_some_action(i) #iterator is not empty
except StopIteration:
    do_empty_action() #iterator is empty

StopIteration 异常很可能是由于调用 it1.next() 导致的,因为在循环内部引发的任何 StopIteration 异常都将终止该循环。

编辑:对于那些不喜欢这种异常的人,可以使用 islice 来设置单步循环:

from itertools import tee, islice
it1, it2 = tee(iterable)
for _ in islice(it1, 1):
    #loop entered if iterator is not empty
    for i in it2:
        do_some_action(i)
    break #if loop entered don't execute the else section
else:
    do_empty_action()

我个人更喜欢第一种风格。但你的看法可能不同。


0

2
KennyTM所说的。一个直接的布尔测试在使用生成器时会失败。 - Manoj Govindan
啊...谢谢,我一直在想为什么大家都写这么复杂的代码。 - Eike
我本来忽略了,但有人点赞了,这个答案不值得得到 +1。抱歉说实话。 - aaronasterling

0

这是Michael MrozekFM的答案的结合:

def with_divisible(n, a, b, f):
    '''apply f to every integer x such that n divides x and a <= x < b'''
    it = (i for i in xrange(a, b) if not i % n)
    for i in it:
        f(i)
    try: i            # test if `it` was empty
    except NameError: print('do something else')

def g(i):
    print i,

with_divisible( 3, 1, 10, g)   # Prints 3 6 9.
with_divisible(33, 1, 10, g)   # Prints "do something else"

0
生成器具有“gi_frame”属性,一旦生成器用尽,该属性将为None,但仅在引发StopIteration之后。如果这是可以接受的,您可以尝试以下内容:
import types

def do(x, f, f_empty):
    if type(x) == types.GeneratorType:
        # generators have a 'gi_frame' property,
        # which is None once the generator is exhausted
        if x.gi_frame:
            # not empty
            return f(x)
        return f_empty(x)
    if x:
        return f(x)
    return f_empty(x)

def nempty(lst):
    print lst, 'not empty'

def empty(lst):
    print 'Twas empty!'

# lists
do([2,3,4], nempty, empty)
do([], nempty, empty)

# generators
do((i for i in range(5)), nempty, empty)
gen = (i for i in range(1))
gen.next()
try:
    gen.next()
except StopIteration:
    pass
do(gen, nempty, empty)

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