ジェネレーターが最初から空であるかどうかはどうすればわかりますか?


146

発電機は何のアイテムを持っていない場合と同様に、テストする簡単な方法がありpeekhasNextisEmptyこれらの線に沿って何かが?


私が間違っている場合は修正してください。ただし、任意のジェネレーターに真に汎用的なソリューションを作成できれば、yieldステートメントにブレークポイントを設定し、「前に戻る」ことができます。それは、利回りでスタックフレームを複製し、StopIterationでそれらを復元することを意味しますか?

まあ、私はそれらをStopIterationに復元するかどうかを推測しますが、少なくともStopIterationはそれが空であると通知します。そう私は睡眠...必要

4
彼がこれを望んでいる理由を私は知っていると思います。テンプレートを使用してWeb開発を行っており、戻り値をCheetahなどのテンプレートに渡す場合、空のリスト[]はFalseyであることが便利です。ジェネレータは、要素が生成されない場合でもtrueです。
jpsimons 2011年

これが私のユースケースです... glob.iglob("filepattern")ユーザーが指定したワイルドカードパターンを使用しています。パターンがどのファイルとも一致しない場合は、ユーザーに警告します。もちろん、これをさまざまな方法で回避できますが、イテレータが空になったかどうかを明確にテストできると便利です。
LarsH 2013年

このソリューションを使用できます:stackoverflow.com/a/11467686/463758
balki

回答:


53

あなたの質問に対する簡単な答え:いいえ、簡単な方法はありません。回避策はたくさんあります。

ジェネレータとは何かという理由から、実際には単純な方法はありません。シーケンスをメモリに保持せずに値のシーケンスを出力する方法。したがって、逆方向のトラバーサルはありません。

必要に応じて、has_next関数を記述したり、ファンシーなデコレータを持つメソッドとしてジェネレータにスラップしたりすることもできます。


2
十分に公正で、それは理にかなっています。ジェネレーターの長さを見つける方法がないことは知っていましたが、それが最初に何かを生成するかどうかを見つける方法を見逃したのではないかと思いました。
Dan

1
ああ、そして参考までに、私は自分の「ファンシーなデコレータ」の提案を実装してみました。ハード。どうやらcopy.deepcopyはジェネレーターでは機能しません。
David Berger

47
「簡単な方法があってはならない」に同意できるかどうかわかりません。コンピュータサイエンスには、メモリにシーケンスを保持せずに値のシーケンスを出力するように設計された多くの抽象化がありますが、プログラマは、「キュー」から値を削除せずに、別の値があるかどうかを確認できます。「逆方向のトラバーサル」を必要としない、単一の先読みなどがあります。それはイテレータ設計がそのような機能を提供しなければならないということではありませんが、それは確かに有用です。ピーク後に最初の値が変わる可能性があることに基づいて反対しているのでしょうか?
LarsH 2013年

9
私は、典型的な実装が必要になるまで値を計算さえしないという根拠に反対しています。インターフェースにこれを強制することもできますが、軽量の実装には最適ではないかもしれません。
David Berger

6
@ S.Lottシーケンスが空であるかどうかを知るためにシーケンス全体を生成する必要はありません。1つの要素に相当するストレージで十分です-私の回答を参照してください。
Mark Ransom

98

提案:

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.

2
最初の要素をで2回返す意味がわかりませんreturn first, itertools.chain([first], rest)
njzk2 14

6
@ njzk2「ピーク」操作(つまり、関数名)を実行していました。wiki「ピークは、データから値を削除せずにコレクションの先頭の値を返す操作です」
John Fouhy

これは、ジェネレーターがNoneを生成するように設計されている場合は機能しません。 def gen(): for pony in range(4): yield None if pony == 2 else pony
ポール

4
@Paul戻り値をよく見てください。発電が行われている場合-すなわち、戻っていないNoneが、調達StopIteration-関数の結果がありますNone。それ以外の場合はタプルですが、そうではありませんNone
モニカの訴訟に資金

これは私の現在のプロジェクトで私を大いに助けました。Pythonの標準ライブラリモジュール 'mailbox.py'のコードに同様の例が見つかりました。 This method is for backward compatibility only. def next(self): """Return the next message in a one-time iteration.""" if not hasattr(self, '_onetime_keys'): self._onetime_keys = self.iterkeys() while True: try: return self[next(self._onetime_keys)] except StopIteration: return None except KeyError: continue
ピア

29

簡単な方法は、ジェネレータが使い果たされた(または空の)場合に使用されるnext()のオプションパラメータを使用することです。例えば:

iterable = some_generator()

_exhausted = object()

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

編集:mehtunguhのコメントで指摘された問題を修正しました。


1
いいえ。これは、生成された最初の値が正しくないジェネレーターでは正しくありません。
mehtunguh

7
object()代わりにclassを使用して、1行短くします_exhausted = object()if next(iterable, _exhausted) is _exhausted:
メッサ2016年

13

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()ます。


4
なぜこれが最善の答えではないのですか?ジェネレーターが戻る場合None
2016年

8
これはおそらく、空であるかどうかをテストするだけでなく、ジェネレータを実際に使用する必要があるためです。
bfontaine

3
それが利用可能である場合は、次の(発電機、なし)を呼び出した瞬間は、あなたが1つのアイテムをスキップしますので、それは悪いです
ネイサンドゥ

正しい、あなたはあなたのジェンの最初の要素を見逃すでしょう、そしてあなたはあなたのジェンが空かどうかテストするのではなくあなたのジェンを消費するでしょう。
AJ

12

最良のアプローチであるIMHOは、特別なテストを回避することです。ほとんどの時間、発電機の使用があるテスト:

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!"

3
このソリューションは、ジェネレーター全体を消費しようとするため、無限のジェネレーターでは使用できなくなります。
ViktorStískala2013

@ViktorStískala:私はあなたの要点を知りません。無限のジェネレーターが結果を生成したかどうかをテストするのは愚かなことです。
vezult 2013年

他の結果を処理しておらず、生成しても役に立たないため、ソリューションにforループの中断が含まれる可能性があることを指摘しておきます。range(10000000)は有限ジェネレーター(Python 3)ですが、それが何かを生成するかどうかを確認するためにすべての項目を調べる必要はありません。
ViktorStískala2013年

1
@ViktorStískala:わかりました。しかし、私のポイントはこれです:一般的に、あなたは実際にジェネレータの出力を操作したいと思っています。私の例では、何も生成されない場合は、それがわかります。それ以外の場合は、意図したとおりに生成された出力を操作します-「ジェネレーターの使用はテストです」。特別なテスト、または無意味に発電機の出力を消費する必要はありません。これを明確にするために、回答を編集しました。
vezult 2013年

8

私は自分自身を使用しないこと、特に1秒のソリューションを、提供することを嫌い、しかし、あなたは絶対にあれば持っていたこれを行うにし、他の回答のように、発電機を消費しないために:

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)

これがジェネレーターの使用方法ではないと私が信じているので、今、私はこのソリューションが本当に好きではありません。


4

この投稿は現時点で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。


1
これはgen、各アイテムに対して1回だけジェネレーターを呼び出すので、副作用はそれほど悪くありません。しかし、それはジェネレーターからプルされたすべてのコピーを格納しますが、経由bではありませんa。そのため、メモリーの影響は、単に実行してlist(gen)それをチェックするのと同じです。
マティアス・フリップ

これには2つの問題があります。1.このitertoolは、かなりの補助ストレージを必要とする場合があります(保存する必要がある一時データの量によって異なります)。一般に、あるイテレーターが別のイテレーターが開始する前にほとんどまたはすべてのデータを使用する場合は、tee()の代わりにlist()を使用する方が高速です。2. T型反復子はスレッドセーフではありません。元の反復可能オブジェクトがスレッドセーフであっても、同じtee()呼び出しによって返された反復子を同時に使用すると、RuntimeErrorが発生する場合があります。
AJ

3

明らかなアプローチで申し訳ありませんが、最善の方法は次のようにすることです:

for item in my_generator:
     print item

これで、使用中にジェネレーターが空であることを検出しました。もちろん、ジェネレーターが空の場合、アイテムは表示されません。

これはコードに正確に適合しない場合がありますが、これはジェネレーターのイディオムの目的です:反復するため、アプローチを少し変更するか、ジェネレーターをまったく使用しない可能性があります。


または...質問者は、なぜ空のジェネレータを検出しようとするのかについてのヒントを提供できますか?
S.Lott、2009年

「ジェネレーターが空なので何も表示されない」という意味ですか?
SilentGhost 2009年

S.Lott。同意する。理由がわかりません。でも、理由があったとしても、代わりにそれぞれのアイテムを使う方が問題になるかもしれないと思います。
Ali Afshar

1
これは、ジェネレーターが空かどうかをプログラムに伝えません。
イーサンファーマン

3

ジェネレータが空かどうかを確認するために必要なのは、次の結果を取得することです。もちろん、その結果を使用する準備ができていない場合は、後で再び返すために保存する必要があります。

これは、既存のイテレータに追加して__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'

反復の開始時だけでなく、いつでも空をチェックできることに注意してください。


これは正しい方向に向かっています。必要なだけ結果を保存して、望みどおりに先を覗くことができるように変更する必要があります。理想的には、ストリームの先頭に任意のアイテムをプッシュできるようにします。プッシュ可能なイテレーターは、私がよく使用する非常に便利な抽象概念です。
sfkleach

@sfkleach複数の先読みのためにこれを複雑にする必要はないと思います。それはそのままで非常に役立ち、質問に答えます。これは古い質問ですが、まだ時折見られるので、自分の答えを残したい場合は、誰かが役立つと思うかもしれません。
Mark Ransom

マークは、彼の解決策が質問に答えるということはまったく正しい。それがキーポイントである。もっと上手く表現すべきだった。私が意味したことは、無制限のプッシュバックを備えたプッシュ可能なイテレーターは、私が非常に有用であることがわかったイディオムであり、実装はおそらくさらに単純であることです。提案されたように、バリアントコードを投稿します。
sfkleach

2

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)

2

このスレッドに落ちて、非常にシンプルで読みやすい答えが欠けていることに気づきました:

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]

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

ジェネレータの最後で発生StopIterationします。あなたの場合、すぐに終わりに到達するので、例外が発生します。しかし、通常、次の値の存在を確認するべきではありません。

あなたができる別のことは:

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

2
これは実際にジェネレータ全体を消費します。悲しいことに、これが望ましい動作か望ましくない動作かは、質問から明らかではありません。
S.Lott、2009年

ジェネレータに「触れる」他の方法と同様に、私はそう思います。
SilentGhost 2009年

私はこれが古いことを理解していますが、「list()」を使用するのは最善の方法ではありません。生成されたリストが空ではないが実際には大きい場合、これは不必要に無駄です
Chris_Rands

1

ジェネレータを使用するに知っておく必要がある場合は、いいえ、簡単な方法はありません。あなたがされるまで待つことができる場合は後に、あなたが発電機を使用している、簡単な方法があります:

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()

1

単にジェネレーターをitertools.chainでラップし、イテラブルの終わりを表すものを2番目のイテラブルとして置き、それをチェックするだけです。

例:

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

iterableでは決して発生しないeog = object()と仮定する代わりに使用してくださいfloat('-inf')
bfontaine

@bfontaine良いアイデア
smac89

1

私の場合、アイテムをマージする関数に渡す前に、ジェネレータのホストが設定されているかどうかを知る必要がありました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"

私の特定の問題には、反復可能オブジェクトが空であるか、まったく同じ数のエントリがあるという特性があります。


1

このソリューションだけが空の反復でも機能することがわかりました。

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)

マークソリューション次のような空の発電機のためにそれを使用することはできません

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

これは、何かが生成されたかどうかを確認しながらイテレータを返し続けるために使用する私の単純なアプローチです。ループが実行されるかどうかを確認するだけです。

        n = 0
        for key, value in iterator:
            n+=1
            yield key, value
        if n == 0:
            print ("nothing found in iterator)
            break

0

ジェネレータをラップする単純なデコレータを次に示します。空の場合は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')

これが有用な1つの例は、コードをテンプレート化することです-すなわちjinja2

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

これによりジェネレーター関数が2回呼び出されるため、ジェネレーターの起動コストが2回発生します。たとえば、ジェネレーター関数がデータベースクエリである場合、これは重要です。
Ian Goldby

0

isliceを使用すると、最初の反復までチェックして、空かどうかを確認するだけで済みます。

itertools import isliceから

def isempty(iterable):
    return list(islice(iterable、1))== []


申し訳ありませんが、これは完全な読み取りです... StopIterationでtry / catchを実行する必要があります
Quin

0

any()の使用についてはどうですか?私はジェネレーターでそれを使用し、それはうまくいきます。ここでこれについて少し説明している男がいます


2
すべてのジェネレーターに「any()」を使用することはできません。複数のデータフレームを含むジェネレータでそれを使用してみました。「DataFrameの真理値があいまいです」というメッセージが表示されました。on(my_generator_of_df)
probitaille 2017年

any(generator)ジェネレーターがキャストできる値を生成することがわかっている場合に機能しますbool-基本的なデータ型(int、stringなど)が機能します。any(generator)ジェネレータが空の場合、またはジェネレータにfalse値しかない場合、たとえば、ジェネレータが0、 ''(空の文字列)、およびFalseを生成する場合、Falseになりますが、それでもFalseになります。あなたがそれを知っている限り、これは意図された振る舞いかもしれないし、そうでないかもしれません:)
ダニエル

0

サイトールズのピーク関数を使用します。

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

この関数が返すイテレータは、引数として渡された元のイテレータと同じです。


-2

sum関数で解決しました。ジェネレーターを返すglob.iglobで使用した例については、以下を参照してください。

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

*これはおそらく巨大なジェネレーターでは機能しませんが、小さなリストではうまく機能するはずです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.