web-dev-qa-db-ja.com

ジェネレータは再帰的にできますか?

私は単純に再帰的なジェネレーターを作成しようとしました。うまくいきませんでした。これは私がやったことです:

def recursive_generator(lis):
    yield lis[0]
    recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

私が手に入れたのは最初のアイテム6

そのようなコードを機能させる方法はありますか?基本的に、再帰スキームでyieldコマンドを上記のレベルに転送しますか?

55
Aguy

これを試して:

def recursive_generator(lis):
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6,3,9,1]):
    print(k)

私はあなたの機能のバグのためにこれが機能しないことを指摘する必要があります。以下に示すように、おそらくlisが空でないことを確認する必要があります。

def recursive_generator(lis):
    if lis:
        yield lis[0]
        yield from recursive_generator(lis[1:])

Python 2.7であり、yield fromこの質問を確認してください。

81
Alec

コードが役に立たなかった理由

コードでは、ジェネレーター関数は次のことを行います。

  1. returns(yields)リストの最初の値
  2. 次に、新しいイテレータオブジェクトを作成します同じジェネレータ関数を呼び出し、リストのスライスを渡します
  3. そして、停止します

反復子の2番目のインスタンスである再帰的に作成されたインスタンスは、繰り返し処理されません。そのため、リストの最初の項目しか取得できません。

ジェネレーター関数は、イテレーターオブジェクト( イテレータープロトコル を実装するオブジェクト)を自動的に作成するのに役立ちますが、それに対して反復する必要があります:いずれかの手動でオブジェクトでnext()メソッドを呼び出すか、反復プロトコルを自動的に使用するループステートメントを使用して呼び出します。

では、ジェネレータを再帰的に呼び出すことはできますか?

答えはyesです。コードに戻って、実際にジェネレーター関数でこれを行いたい場合は、試してみてください:

_def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it... 
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list.
            yield i
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)
_

注:アイテムは逆の順序で返されるため、最初にジェネレーターを呼び出す前にsome_list.reverse()を使用することをお勧めします。

この例で注意すべき重要な点は、ジェネレーター関数が、forloopで再帰的に自身を呼び出すことです。 、イテレータを見て、そのイテレータを自動的に使用するため、実際に値を取得します。

これは機能しますが、これは実際には役に立たないと思います。ジェネレーター関数を使用してリストを反復処理し、アイテムを一度に1つずつ取り出しますが、...リストは反復可能なため、ジェネレーターは不要です!もちろんわかりますが、これは単なる例であり、このアイデアの有用なアプリケーションがあるかもしれません。

もう一つの例

前の例をリサイクルしましょう(遅延のため)。リスト内のアイテムを印刷し、すべてのアイテムに前のアイテムの数を追加する必要があるとしましょう(ランダムな例であり、必ずしも有用ではありません)。

コードは次のようになります。

_def recursive_generator(some_list):
    """
    Return some_list items, one at a time, recursively iterating over a slice of it...
    and adding to every item the count of previous items in the list
    """
    if len(some_list)>1:
    # some_list has more than one item, so iterate over it
        for i in recursive_generator(some_list[1:]):
            # recursively call this generator function to iterate over a slice of some_list.
            # return one item from the list, but add 1 first. 
            # Every recursive iteration will add 1, so we basically add the count of iterations.
            yield i + 1
        else:
            # the iterator returned StopIteration, so the for loop is done.
            # to finish, return the only value not included in the slice we just iterated on.
            yield some_list[0]
    else:
        # some_list has only one item, no need to iterate on it.
        # just return the item.
        yield some_list[0]

some_list = [6,3,9,1]
for k in recursive_generator(some_list):
    print(k)
_

ご覧のとおり、ジェネレーター関数はリストアイテムを返す前に実際に何かを実行しており、再帰の使用が理にかなっています。それでも、単なる愚かな例ですが、あなたはアイデアを得る。

注:もちろん、この愚かな例では、リストには数字のみが含まれることが期待されます。試してみて、それを壊したい場合は、some_listに文字列を入力して楽しんでください。繰り返しますが、これは単なる例であり、productionコードではありません!

21
Daniele Barresi

再帰的なジェネレーターは、非線形構造を横断するのに役立ちます。たとえば、バイナリツリーをNoneまたはTuple of value、left tree、right treeとします。再帰ジェネレーターは、すべてのノードにアクセスする最も簡単な方法です。例:

_tree = (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))),
        (6, None, (7, (8, (9, None, None), None), None)))

def visit(tree):  # 
    if tree is not None:
        try:
            value, left, right = tree
        except ValueError:  # wrong number to unpack
            print("Bad tree:", tree)
        else:  # The following is one of 3 possible orders.
            yield from visit(left)
            yield value  # Put this first or last for different orders.
            yield from visit(right)

print(list(visit(tree)))

# prints nodes in the correct order for 'yield value' in the middle.
# [1, 3, 2, 5, 4, 0, 6, 9, 8, 7]
_

編集:_if tree_を_if tree is not None_に置き換えて、他の偽の値をエラーとしてキャッチします。

編集2: try:句に再帰呼び出しを入れることについて(@ jpmc26によるコメント)。

不良ノードの場合、上記のコードはValueErrorをログに記録して続行します。たとえば、_(9,None,None)_が_(9,None)_に置き換えられた場合、出力は

_Bad tree: (9, None)
[1, 3, 2, 5, 4, 0, 6, 8, 7]
_

より典型的なのは、ロギング後にリレイズして、出力が

_Bad tree: (9, None)
Traceback (most recent call last):
  File "F:\Python\a\tem4.py", line 16, in <module>
    print(list(visit(tree)))
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 14, in visit
    yield from visit(right)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 12, in visit
    yield from visit(left)
  File "F:\Python\a\tem4.py", line 7, in visit
    value, left, right = tree
ValueError: not enough values to unpack (expected 3, got 2)
_

トレースバックは、ルートから不良ノードへのパスを提供します。元のvisit(tree)呼び出しをラップして、トレースバックをパス(ルート、右、右、左、左)に減らすことができます。

再帰呼び出しがtry:句に含まれている場合、エラーはツリーの各レベルで検出、再記録、および再発生されます。

_Bad tree: (9, None)
Bad tree: (8, (9, None), None)
Bad tree: (7, (8, (9, None), None), None)
Bad tree: (6, None, (7, (8, (9, None), None), None))
Bad tree: (0, (1, None, (2, (3, None, None), (4, (5, None, None), None))), (6, None, (7, (8, (9, None), None), None)))
Traceback (most recent call last):
...  # same as before
_

複数のロギングレポートは、ヘルプよりもノイズが多い可能性があります。不良ノードへのパスが必要な場合は、各再帰呼び出しを独自のtry:句でラップし、これまでに構築されたパスを使用して、各レベルで新しいValueErrorを発生させるのが最も簡単な場合があります。

結論:フロー制御に例外を使用していない場合(たとえばIndexErrorを使用して)、try:ステートメントの存在と配置は、必要なエラーレポートに依存します。

10
Terry Jan Reedy

最大Python 3.4、終了時にStopIteration例外を発生させなければならなかったジェネレーター関数。再帰ケースの場合、他の例外(たとえばIndexError)はStopIterationより前に発生したため、手動で追加します。

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis[0]
    yield from recursive_generator(lis[1:])

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

def recursive_generator(lis):
    if not lis: raise StopIteration
    yield lis.pop(0)
    yield from recursive_generator(lis)

for k in recursive_generator([6, 3, 9, 1]):
    print(k)

forループはStopIteration例外をキャッチすることに注意してください。これについての詳細 here

1
Levon

はい、再帰ジェネレーターを使用できます。ただし、他の再帰関数と同じ再帰深度制限の影響を受けます。

def recurse(x):
  yield x
  yield from recurse(x)

for (i, x) in enumerate(recurse(5)):
  print(i, x)

このループは、クラッシュする前に約3000(私にとって)になります。

ただし、いくつかのトリックを使用して、ジェネレーターをそれ自体に供給する関数を作成できます。これにより、再帰的なジェネレーターを作成できますが、そうではありません: https://Gist.github.com/3noch/7969f416d403ba3a54a788b113c204ce

0
Elliot Cameron