【问题标题】:How do I know if a generator is empty from the start?我如何知道生成器是否从一开始就为空?
【发布时间】:2016-05-12 16:56:22
【问题描述】:

是否有一种简单的方法来测试生成器是否没有项目,例如peekhasNextisEmpty,类似这些内容?

【问题讨论】:

  • 如果我错了,请纠正我,但如果您可以为 any 生成器提供真正通用的解决方案,则相当于在 yield 语句上设置断点并具有“后退”的能力。这是否意味着在产量上克隆堆栈帧并在 StopIteration 上恢复它们​​?
  • 好吧,我猜想是否恢复它们 StopIteration ,但至少 StopIteration 会告诉你它是空的。是的,我需要睡觉......
  • 我想我知道他为什么想要这个。如果您正在使用模板进行 Web 开发,并将返回值传递给像 Cheetah 之类的模板,那么空列表 [] 很方便 Falsey,因此您可以对其进行 if 检查并为某事或什么都不做特殊行为。生成器是真的,即使它们不产生任何元素。
  • 这是我的用例...我在用户提供的通配符模式上使用glob.iglob("filepattern"),如果该模式与任何文件都不匹配,我想警告用户。当然我可以通过各种方式解决这个问题,但是能够干净地测试迭代器是否为空是很有用的。
  • 可以使用这个解决方案:stackoverflow.com/a/11467686/463758

标签: python generator


【解决方案1】:

bool(generator) 会返回正确的结果

【讨论】:

    【解决方案2】:

    一个简单的方法是使用next() 的可选参数,如果生成器耗尽(或为空),则使用该参数。例如:

    _exhausted  = object()
    
    if next(some_generator, _exhausted) is _exhausted:
        print('generator is empty')
    

    【讨论】:

    • 为什么是对象和所有这些东西?简单地说:if next(itreable,-1) == -1 然后 gen 是空的!
    • @Apostolos 因为next(iter([-1, -2, -3]), -1) == -1True。换句话说,任何第一个元素等于-1 的可迭代对象都将使用您的条件显示为空。
    • @Apostolos 在简单的情况下,是的,这就是解决方案。但是,如果您计划为任何可迭代对象创建通用工具,则它会失败。
    • @Apostolos object() 是生成器中不包含的非凡值。
    • 注:这仍然是一个“窥视”功能,将从生成器中取出一个元素。
    【解决方案3】:

    我发现只有这个解决方案也适用于空迭代。

    def is_generator_empty(generator):
        a, b = itertools.tee(generator)
        try:
            next(a)
        except StopIteration:
            return True, b
        return False, b
    
    is_empty, generator = is_generator_empty(generator)
    

    或者如果您不想为此使用异常,请尝试使用

    def is_generator_empty(generator):
        a, b = itertools.tee(generator)
        for item in a:
            return False, b
        return True, b
    
    is_empty, generator = is_generator_empty(generator)
    

    marked solution 中,您不能将它用于像这样的空生成器

    def get_empty_generator():
        while False:
            yield None 
    
    generator = get_empty_generator()
    

    【讨论】:

      【解决方案4】:

      为了帮助我的“2美分”,我将描述我的经历:

      我有一个生成器,我需要使用itertools.islice 将它切成小生成器。然后检查我的子生成器是否为空,我只需将它们转换/使用为一个小列表,然后检查列表是否为空。

      例如:

      from itertools import islice
      
      def generator(max_yield=10):
          a = 0
      
          while True:
              a += 1
      
              if a > max_yield:
                  raise StopIteration()
      
              yield a
      
      tg = generator()
      
      label = 1
      
      while True:
          itg = list(islice(tg, 3))
      
          if not itg:  # <-- I check if the list is empty or not
              break
      
          for i in itg:
              print(f'#{label} - {i}')
      
          label += 1
      

      输出:

      #1 - 1
      #1 - 2
      #1 - 3
      #2 - 4
      #2 - 5
      #2 - 6
      #3 - 7
      #3 - 8
      #3 - 9
      #4 - 10
      

      也许这不是最好的方法,主要是因为它消耗了生成器,但它对我有用。

      【讨论】:

        【解决方案5】:

        有一个非常简单的解决方案:if next(generator,-1) == -1 然后生成器为空!

        【讨论】:

        • 这将消耗生成器。
        • 回顾一下:问题是关于消费前的检查。
        • 你在说什么消费?这在开始时完成一次!我的解决方案肯定没有错
        • 虽然这并没有回答所陈述的确切问题,但我会赞成它,因为它确实处理了一个常见的情况,即找出生成器是否会返回任何东西。很多时候我发现自己想写matches = filter(lambda x: ..., my_list); return next(matches) if any_results(matches) else None之类的东西。我才知道这可以写成matches = filter(lambda x: ..., my_list); return next(matches, None)
        • 谢谢。很高兴看到有更多理性的人! :))
        【解决方案6】:

        这是一个古老且已回答的问题,但之前没有人展示过,这里是:

        for _ in generator:
            break
        else:
            print('Empty')
        

        You can read more here

        【讨论】:

        • 但是,当您确实想对生成器项目进行实际操作时,这有什么用呢?只是在主代码之前插入这个 sn-p 看起来很脏 WA
        • 如果生成器产生多个项目,这显然不起作用。
        【解决方案7】:

        刚接触到这个帖子,发现缺少一个非常简单易读的答案:

        def is_empty(generator):
            for item in generator:
                return False
            return True
        

        如果我们不打算消耗任何物品,那么我们需要将第一个物品重新注入生成器:

        def is_empty_no_side_effects(generator):
            try:
                item = next(generator)
                def my_generator():
                    yield item
                    yield from generator
                return my_generator(), False
            except StopIteration:
                return (_ for _ in []), True
        

        例子:

        >>> g=(i for i in [])
        >>> g,empty=is_empty_no_side_effects(g)
        >>> empty
        True
        >>> g=(i for i in range(10))
        >>> g,empty=is_empty_no_side_effects(g)
        >>> empty
        False
        >>> list(g)
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        

        【讨论】:

          【解决方案8】:

          由 Mark Ransom 提示,这里有一个类,您可以使用它来包装任何迭代器,以便您可以提前查看、将值推回流中并检查是否为空。这是一个简单的想法和一个简单的实现,我过去发现它非常方便。

          class Pushable:
          
              def __init__(self, iter):
                  self.source = iter
                  self.stored = []
          
              def __iter__(self):
                  return self
          
              def __bool__(self):
                  if self.stored:
                      return True
                  try:
                      self.stored.append(next(self.source))
                  except StopIteration:
                      return False
                  return True
          
              def push(self, value):
                  self.stored.append(value)
          
              def peek(self):
                  if self.stored:
                      return self.stored[-1]
                  value = next(self.source)
                  self.stored.append(value)
                  return value
          
              def __next__(self):
                  if self.stored:
                      return self.stored.pop()
                  return next(self.source)
          

          【讨论】:

            【解决方案9】:

            查看生成器是否为空所需要做的就是尝试获取下一个结果。当然,如果您还没有准备好使用该结果,那么您必须存储它以便稍后再次返回。

            这是一个包装类,可以添加到现有迭代器中以添加__nonzero__ 测试,因此您可以使用简单的if 来查看生成器是否为空。它可能也可以变成装饰器。

            class GenWrapper:
                def __init__(self, iter):
                    self.source = iter
                    self.stored = False
            
                def __iter__(self):
                    return self
            
                def __nonzero__(self):
                    if self.stored:
                        return True
                    try:
                        self.value = next(self.source)
                        self.stored = True
                    except StopIteration:
                        return False
                    return True
            
                def __next__(self):  # use "next" (without underscores) for Python 2.x
                    if self.stored:
                        self.stored = False
                        return self.value
                    return next(self.source)
            

            以下是您的使用方法:

            with open(filename, 'r') as f:
                f = GenWrapper(f)
                if f:
                    print 'Not empty'
                else:
                    print 'Empty'
            

            请注意,您可以随时检查是否为空,而不仅仅是在迭代开始时。

            【讨论】:

            • 这是正确的方向。应该对其进行修改以允许尽可能多地查看,并根据需要存储尽可能多的结果。理想情况下,它将允许将任意项目推送到流的头部。 pushable-iterator 是我经常使用的非常有用的抽象。
            • @sfkleach 我不认为有必要将其复杂化以进行多次窥视,它非常有用并回答了这个问题。尽管这是一个老问题,但它仍然偶尔会出现,所以如果你想留下自己的答案,有人可能会觉得它很有用。
            • 马克非常正确,他的解决方案回答了问题,这是关键。我应该更好地表达它。我的意思是,具有无限后推的可推送迭代器是我发现非常有用的一个习惯用法,并且实现可以说更简单。按照建议,我将发布变体代码。
            【解决方案10】:

            在 cytoolz 中使用peek 函数。

            from cytoolz import peek
            from typing import Tuple, Iterable
            
            def is_empty_iterator(g: Iterable) -> Tuple[Iterable, bool]:
                try:
                    _, g = peek(g)
                    return g, False
                except StopIteration:
                    return g, True
            

            此函数返回的迭代器将等同于作为参数传入的原始迭代器。

            【讨论】:

              【解决方案11】:

              next(generator, None) is not None

              或者替换None,但无论你知道它是什么值,它不是在你的生成器中。

              编辑:是的,这将跳过生成器中的 1 个项目。然而,我经常检查生成器是否为空只是出于验证目的,然后并没有真正使用它。否则我会做类似的事情:

              def foo(self):
                  if next(self.my_generator(), None) is None:
                      raise Exception("Not initiated")
              
                  for x in self.my_generator():
                      ...
              

              也就是说,如果您的 generator 来自 函数,则此方法有效,例如 generator()

              【讨论】:

              • 为什么这不是最佳答案?如果生成器返回None?
              • 可能是因为这会迫使您实际使用生成器,而不仅仅是测试它是否为空。
              • 这很糟糕,因为当您调用 next(generator, None) 时,如果它可用,您将跳过 1 个项目
              • 正确,你会错过你的 gen 的第一个元素,而且你会消耗你的 gen 而不是测试它是否为空。
              • 这不是一个通用的解决方案,因为它只适用于那些我们事先知道生成器永远不会返回的值的生成器,例如,None
              【解决方案12】:

              在我的例子中,我需要知道是否填充了许多生成器,然后再将其传递给合并项目的函数,即zip(...)。解决方案与接受的答案相似,但又足够不同:

              定义:

              def has_items(iterable):
                  try:
                      return True, itertools.chain([next(iterable)], iterable)
                  except StopIteration:
                      return False, []
              

              用法:

              def filter_empty(iterables):
                  for iterable in iterables:
                      itr_has_items, iterable = has_items(iterable)
                      if itr_has_items:
                          yield iterable
              
              
              def merge_iterables(iterables):
                  populated_iterables = filter_empty(iterables)
                  for items in zip(*populated_iterables):
                      # Use items for each "slice"
              

              我的特殊问题具有可迭代对象为空或具有完全相同数量的条目的属性。

              【讨论】:

                【解决方案13】:

                只需用itertools.chain 包装生成器,将代表迭代结束的内容作为第二个迭代,然后简单地检查它。

                例如:

                import itertools
                
                g = some_iterable
                eog = object()
                wrap_g = itertools.chain(g, [eog])
                

                现在剩下的就是检查我们附加到可迭代末尾的那个值,当你读到它时,那将表示结束

                for value in wrap_g:
                    if value == eog: # DING DING! We just found the last element of the iterable
                        pass # Do something
                

                【讨论】:

                • 使用eog = object() 而不是假设float('-inf') 永远不会出现在可迭代对象中。
                • @bfontaine 好主意
                【解决方案14】:

                这是一个包装生成器的简单装饰器,因此如果为空则返回 None。如果您的代码需要知道生成器是否会在 循环之前生成任何内容,这将很有用。

                def generator_or_none(func):
                    """Wrap a generator function, returning None if it's empty. """
                
                    def inner(*args, **kwargs):
                        # peek at the first item; return None if it doesn't exist
                        try:
                            next(func(*args, **kwargs))
                        except StopIteration:
                            return None
                
                        # return original generator otherwise first item will be missing
                        return func(*args, **kwargs)
                
                    return inner
                

                用法:

                import random
                
                @generator_or_none
                def random_length_generator():
                    for i in range(random.randint(0, 10)):
                        yield i
                
                gen = random_length_generator()
                if gen is None:
                    print('Generator is empty')
                

                一个有用的例子是在模板代码中 - 即 jinja2

                {% if content_generator %}
                  <section>
                    <h4>Section title</h4>
                    {% for item in content_generator %}
                      {{ item }}
                    {% endfor %
                  </section>
                {% endif %}
                

                【讨论】:

                • 这会调用两次生成器函数,因此会产生两次生成器的启动成本。例如,如果生成器函数是数据库查询,那么这可能会很重要。
                【解决方案15】:

                我使用 sum 函数解决了这个问题。下面是我与 glob.iglob 一起使用的示例(它返回一个生成器)。

                def isEmpty():
                    files = glob.iglob(search)
                    if sum(1 for _ in files):
                        return True
                    return False
                

                *这可能不适用于 HUGE 生成器,但应该适用于较小的列表

                【讨论】:

                  【解决方案16】:

                  建议:

                  def peek(iterable):
                      try:
                          first = next(iterable)
                      except StopIteration:
                          return None
                      return first, itertools.chain([first], iterable)
                  

                  用法:

                  res = peek(mysequence)
                  if res is None:
                      # sequence is empty.  Do stuff.
                  else:
                      first, mysequence = res
                      # Do something with first, maybe?
                      # Then iterate over the sequence:
                      for element in mysequence:
                          # etc.
                  

                  【讨论】:

                  • 我不太明白在return first, itertools.chain([first], rest) 中两次返回第一个元素的意义。
                  • @njzk2 我要进行“窥视”操作(因此是函数名称)。 wiki "peek 是一种操作,它返回集合顶部的值而不从数据中删除值"
                  • 如果生成器被设计为产生无,这将不起作用。 def gen(): for pony in range(4): yield None if pony == 2 else pony
                  • @Paul 仔细查看返回值。如果生成器完成——即,不返回None,而是提高StopIteration——函数的结果是None。否则为元组,不是None
                  • 大量的peek 调用不会创建一个永无止境的itertools.chain 对象链,其中包含对其他itertools.chain 对象的引用?
                  【解决方案17】:

                  我意识到此时这篇文章已有 5 年历史,但我在寻找一种惯用的方法时发现了它,但没有看到我发布的解决方案。所以为了后代:

                  import itertools
                  
                  def get_generator():
                      """
                      Returns (bool, generator) where bool is true iff the generator is not empty.
                      """
                      gen = (i for i in [0, 1, 2, 3, 4])
                      a, b = itertools.tee(gen)
                      try:
                          a.next()
                      except StopIteration:
                          return (False, b)
                      return (True, b)
                  

                  当然,我相信许多评论员会指出,这很老套,并且仅在某些有限的情况下才有效(例如,生成器没有副作用)。 YMMV。

                  【讨论】:

                  • 这只会为每个项目调用一次gen 生成器,因此副作用不是太严重的问题。但它会存储通过b而不是通过a从生成器中提取的所有内容的副本,因此内存影响类似于仅运行list(gen)并检查它。
                  • 它有两个问题。 1.这个itertool可能需要大量的辅助存储(取决于需要存储多少临时数据)。一般来说,如果一个迭代器在另一个迭代器启动之前使用了大部分或全部数据,那么使用 list() 而不是 tee() 会更快。 2. tee 迭代器不是线程安全的。当同时使用由同一个 tee() 调用返回的迭代器时,可能会引发 RuntimeError,即使原来的可迭代对象是线程安全的。
                  【解决方案18】:

                  恕我直言,最好的方法是避免特殊测试。大多数时候,使用生成器测试:

                  thing_generated = False
                  
                  # Nothing is lost here. if nothing is generated, 
                  # the for block is not executed. Often, that's the only check
                  # you need to do. This can be done in the course of doing
                  # the work you wanted to do anyway on the generated output.
                  for thing in my_generator():
                      thing_generated = True
                      do_work(thing)
                  

                  如果这还不够好,您仍然可以执行显式测试。此时,thing 将包含最后生成的值。如果没有生成任何内容,它将是未定义的 - 除非您已经定义了变量。您可以检查thing 的值,但这有点不可靠。相反,只需在块中设置一个标志并在之后检查它:

                  if not thing_generated:
                      print "Avast, ye scurvy dog!"
                  

                  【讨论】:

                  • 此解决方案将尝试消耗整个生成器,从而使其无法用于无限生成器。
                  • @ViktorStískala:我不明白你的意思。测试无限生成器是否产生任何结果是愚蠢的。
                  • 我想指出,您的解决方案可能在 for 循环中包含 break,因为您没有处理其他结果并且生成它们是无用的。 range(10000000) 是有限生成器(Python 3),但您不需要遍历所有项目来确定它是否生成了一些东西。
                  • @ViktorStískala:明白。但是,我的观点是:通常,您实际上希望对生成器输出进行操作。在我的示例中,如果没有生成任何内容,您现在就知道了。否则,您按预期对生成的输出进行操作 - “使用生成器就是测试”。无需特殊测试,或无意义地消耗发电机输出。我已经编辑了我的答案以澄清这一点。
                  【解决方案19】:

                  如果您需要在使用生成器之前了解,那么不,没有简单的方法。如果你可以等到你使用了生成器之后,有一个简单的方法:

                  was_empty = True
                  
                  for some_item in some_generator:
                      was_empty = False
                      do_something_with(some_item)
                  
                  if was_empty:
                      handle_already_empty_generator_case()
                  

                  【讨论】:

                    【解决方案20】:

                    您的问题的简单答案:不,没有简单的方法。有很多变通方法。

                    真的不应该有一个简单的方法,因为生成器是:一种输出值序列的方法而不将序列保存在内存中。所以没有反向遍历。

                    如果你愿意,你可以编写一个 has_next 函数,或者甚至可以将它作为一个带有精美装饰器的方法添加到生成器中。

                    【讨论】:

                    • 很公平,这是有道理的。我知道没有办法找到发电机的长度,但我想我可能错过了找到它最初是否会产生任何东西的方法。
                    • 哦,作为参考,我尝试实施我自己的“花式装饰器”建议。难的。显然 copy.deepcopy 不适用于生成器。
                    • 我不确定我是否同意“不应该有简单的方法”。计算机科学中有许多抽象设计用于输出一系列值而不将序列保存在内存中,但这允许程序员询问是否存在另一个值,而无需将其从“队列”中删除(如果有)。无需“向后遍历”即可进行单次预览。这并不是说迭代器设计必须提供这样的功能,但它确实很有用。也许您反对的理由是第一个值可能会在窥视后发生变化?
                    • 我反对的理由是一个典型的实现在需要之前甚至不会计算一个值。可以强制接口执行此操作,但这对于轻量级实现可能不是最佳的。
                    • @S.Lott 您无需生成整个序列即可知道序列是否为空。一个元素的存储空间就足够了 - 请参阅我的答案。
                    【解决方案21】:

                    我讨厌提供第二种解决方案,尤其是我自己不会使用的解决方案,但是,如果您绝对必须这样做并且不消耗生成器,就像其他答案一样:

                    def do_something_with_item(item):
                        print item
                    
                    empty_marker = object()
                    
                    try:
                         first_item = my_generator.next()     
                    except StopIteration:
                         print 'The generator was empty'
                         first_item = empty_marker
                    
                    if first_item is not empty_marker:
                        do_something_with_item(first_item)
                        for item in my_generator:
                            do_something_with_item(item)
                    

                    现在我真的不喜欢这个解决方案,因为我相信这不是生成器的使用方式。

                    【讨论】:

                      【解决方案22】:
                      >>> gen = (i for i in [])
                      >>> next(gen)
                      Traceback (most recent call last):
                        File "<pyshell#43>", line 1, in <module>
                          next(gen)
                      StopIteration
                      

                      在生成器的末尾StopIteration 被引发,因为在你的情况下立即到达 end,引发异常。 但通常您不应该检查下一个值是否存在。

                      你可以做的另一件事是:

                      >>> gen = (i for i in [])
                      >>> if not list(gen):
                          print('empty generator')
                      

                      【讨论】:

                      • 这实际上消耗了整个生成器。可悲的是,从问题中不清楚这是可取的还是不可取的行为。
                      • 我想,就像“触摸”生成器的任何其他方式一样。
                      • 我知道这是旧的,但使用 'list()' 并不是最好的方法,如果生成的列表不是空的但实际上很大,那么这是不必要的浪费
                      【解决方案23】:

                      对于显而易见的方法感到抱歉,但最好的方法是:

                      for item in my_generator:
                           print item
                      

                      现在您在使用时检测到生成器是空的。当然,如果生成器为空,项目将永远不会显示。

                      这可能不完全适合您的代码,但这就是生成器的用语:迭代,因此您可能会稍微改变您的方法,或者根本不使用生成器。

                      【讨论】:

                      • 或者...提问者可以提供一些提示,说明为什么会尝试检测空生成器?
                      • 您的意思是“由于生成器为空,因此不会显示任何内容”?
                      • S.Lott.我同意。我不明白为什么。但我认为即使有原因,问题最好还是转而使用每个项目。
                      • 这不会告诉程序生成器是否为空。
                      猜你喜欢
                      • 2013-06-20
                      • 2015-04-28
                      • 2018-09-24
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多