web-dev-qa-db-ja.com

yieldはStopIteration例外をどのようにキャッチしますか?

例の関数で終了する理由:

def func(iterable):
    while True:
        val = next(iterable)
        yield val

しかし、yieldステートメント関数をオフにするとStopIteration例外が発生しますか?

EDIT:皆さんを誤解させてすみません。ジェネレーターとは何か、そしてその使用方法を知っています。もちろん、関数が終了すると言ったとき、私は関数の熱心な評価を意味しませんでした。私は関数を使用してジェネレーターを生成するとき、それを暗示しました:

gen = func(iterable)

funcの場合は動作し、同じジェネレーターを返しますが、func2の場合:

def func2(iterable):
    while True:
        val = next(iterable)

Nonereturnまたは無限ループの代わりにStopIterationを発生させます。

もっと具体的にさせてください。関数teeinitertoolsは次と同等です:

def tee(iterable, n=2):
    it = iter(iterable)
    deques = [collections.deque() for i in range(n)]
    def gen(mydeque):
        while True:
            if not mydeque:             # when the local deque is empty
                newval = next(it)       # fetch a new value and
                for d in deques:        # load it to all the deques
                    d.append(newval)
            yield mydeque.popleft()
    return Tuple(gen(d) for d in deques)

実際、ネストされた関数genにはbreakステートメントのない無限ループがあるため、いくつかの魔法があります。 gen関数は、itにアイテムがない場合、StopIteration例外により終了します。ただし、例外は発生せずに正しく終了します。つまり、ループを停止します。 だから質問はです:StopIterationはどこで処理されますか?

23
Sergey Ivanov

StopIterationが_itertools.tee_内で作成されたgenジェネレーターのどこでキャッチされるかという質問に答えるために、そうではありません。 teeの結果のコンシューマーは、例外を繰り返し処理するときにキャッチします。

まず、ジェネレーター関数(yieldステートメントを含む任意の関数)は通常の関数とは根本的に異なることに注意することが重要です。関数の呼び出し時に関数のコードを実行する代わりに、関数を呼び出すときにgeneratorオブジェクトを取得します。ジェネレーターを反復処理する場合にのみ、コードを実行します。

ジェネレーター関数は、StopIterationを発生させずに反復を終了することはありません(代わりに他の例外を発生させない限り)。 StopIterationは、ジェネレーターから実行されたというシグナルであり、オプションではありません。 returnステートメントまたはジェネレーター関数のコードの最後に何も上げることなく到達すると、PythonはStopIterationを上げます!

これは、他に何も返さずに最後に達した場合にNoneを返す通常の関数とは異なります。上で説明したように、ジェネレーターが動作するさまざまな方法と関連しています。

StopIterationがどのように発生するかを簡単に確認できるジェネレータ関数の例を次に示します。

_def simple_generator():
    yield "foo"
    yield "bar"
    # StopIteration will be raised here automatically
_

消費すると次のようになります。

_>>> g = simple_generator()
>>> next(g)
'foo'
>>> next(g)
'bar'
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    next(g)
StopIteration
_

_simple_generator_を呼び出すと、常にすぐにgeneratorオブジェクトが返されます(関数内のコードを実行せずに)。ジェネレーターオブジェクトでnextを呼び出すたびに、次のyieldステートメントまでコードが実行され、生成された値が返されます。取得する必要がない場合は、StopIterationが発生します。

現在、通常、StopIteration例外は表示されません。これは、通常、forループ内でジェネレーターを使用するためです。 forステートメントは、nextが発生するまで、自動的にStopIterationを繰り返し呼び出します。 StopIteration例外をキャッチして抑制しますので、try/exceptブロックをいじる必要はありません。

for item in iterable: do_suff(item)のようなforループは、このwhileループとほぼ同じです(唯一の違いは、実際のforは一時変数を必要としないことです)イテレータを保持するため)

_iterator = iter(iterable)
try:
    while True:
        item = next(iterator)
        do_stuff(item)
except StopIteration:
    pass
finally:
    del iterator
_

上に示したgenジェネレーター関数は1つの例外です。反復処理が完了したという独自のシグナルであるため、消費する反復子によって生成されたStopIteration例外を使用します。つまり、StopIterationをキャッチしてからループを抜けるのではなく、例外をキャッチしないようにします(おそらく、より高いレベルのコードでキャッチされます)。

主な質問とは無関係に、もう1つ指摘したいことがあります。コードでは、nextという変数でiterableを呼び出しています。取得するオブジェクトの種類のドキュメントとしてその名前を使用する場合、これは必ずしも安全ではありません。

nextiterator(またはiterable)プロトコルではなく、containerプロトコルの一部です。一部の種類の反復可能オブジェクト(ファイルやジェネレーターなど、独自の反復子である)で機能する場合もありますが、タプルやリストなど、その他の反復可能オブジェクトでは失敗します。より適切なアプローチは、iter値でiterableを呼び出し、受け取ったイテレーターでnextを呼び出すことです。 (または、適切なタイミングでforiterの両方を呼び出すnextループを使用してください!)

編集:関連する質問のGoogle検索で自分の答えを見つけましたが、上記の答えが将来的に完全に真実ではないことを指摘するために更新するつもりだったPythonバージョン。 PEP 479 は、StopIterationがジェネレーター関数からキャッチされないようにすることをエラーにしています。その場合、Pythonは代わりにRuntimeError例外。

これは、itertoolsを使用してジェネレーター関数から抜け出すStopIterationの例のようなコードを変更する必要があることを意味します。通常、try/exceptを使用して例外をキャッチし、returnを実行する必要があります。

これは後方互換性のない変更であるため、徐々に段階的に導入されています。 In Python 3.5、すべてのコードはデフォルトで以前と同様に動作しますが、_from __future__ import generator_stop_を使用して新しい動作を取得できます。InPython 3.6、コードは引き続き機能しますが、警告が表示されます。Python 3.7では、新しい動作が常に適用されます。

42
Blckknght

関数にyieldが含まれる場合、呼び出しても実際には何も実行されず、ジェネレーターオブジェクトが作成されるだけです。このオブジェクトを反復処理するだけでコードが実行されます。だから私の推測では、単に関数を呼び出しているだけです。つまり、関数はStopIterationを発生させません。これは、実行されないため

あなたの関数と反復可能なものを考えます:

_def func(iterable):
    while True:
        val = next(iterable)
        yield val

iterable = iter([1, 2, 3])
_

これは間違った呼び出し方法です:

_func(iterable)
_

これは正しい方法です:

_for item in func(iterable):
    # do something with item
_

ジェネレーターを変数に保存し、その上でnext()を呼び出す(または他の方法で反復する)こともできます。

_gen = func(iterable)
print(next(gen))   # prints 1
print(next(gen))   # prints 2
print(next(gen))   # prints 3
print(next(gen))   # StopIteration
_

ところで、関数を記述するより良い方法は次のとおりです。

_def func(iterable):
    for item in iterable:
        yield item
_

またはPython 3.3以降:

_def func(iterable):
    yield from iter(iterable)
_

もちろん、実際の発電機がそれほど些細なことはめったにありません。 :-)

6
kindall

yieldがなければ、iterableで何もすることを止めずにval全体を反復処理します。 whileループはStopIteration例外をキャッチしません。同等のforループは次のようになります。

def func(iterable):
    for val in iterable:
        pass

StopIterationをキャッチし、単純にループを終了して関数から戻ります。

明示的に例外をキャッチできます:

def func(iterable):
    while True:
        try:
            val = next(iterable)
        except StopIteration:
            break
3
chepner

yieldStopIterationをキャッチしません。 yieldが関数に対して行うことは、それが通常の関数ではなくジェネレーター関数になることです。したがって、関数呼び出しから返されるオブジェクトは、反復可能なオブジェクトです(next関数(forループによって暗黙的に呼び出される)を使用して次の値を計算すると、次の値が計算されます)。 yieldステートメントを省略すると、pythonはwhileループ全体をすぐに実行し、イテレート可能ファイルを使い果たします(有限の場合) StopIterationを呼び出したときにすぐに呼び出します。

考慮してください:

x = func(x for x in [])
next(x)  #raises StopIteration

forループが例外をキャッチします-これは、与えられたイテレート可能オブジェクトでnextの呼び出しをいつ停止するかを知る方法です。

0
mgilson