そのような質問に答えようとするとき、あなたは本当にあなたが解決策として提案するコードの制限を与える必要があります。パフォーマンスのみであれば、あまり気にしませんが、解決策として提案されたコード(承認された回答を含む)のほとんどは、深さが1000を超えるリストを平坦化できません。
私が言うときのコードのほとんどを私は再帰のいずれかの形式を使用するすべてのコードを意味する(または再帰的である標準ライブラリ関数を呼び出します)。行われた再帰呼び出しごとに、(呼び出し)スタックが1単位大きくなり、(デフォルト)Python呼び出しスタックのサイズが1000になるため、これらのコードはすべて失敗します。
コールスタックに精通していない場合は、次の方法が役立つ場合があります(それ以外の場合は、実装までスクロールできます)。
呼び出しスタックのサイズと再帰プログラミング(ダンジョンの類推)
宝物を見つけて終了する
宝物を探して、番号の付いた部屋のある巨大なダンジョンに入ると想像してみてください。あなたは場所を知りませんが、宝物を見つける方法についていくつかの目安があります。それぞれの兆候はなぞなぞです(難易度はさまざまですが、どれほど難しいかは予測できません)。あなたは時間を節約するための戦略について少し考えることに決め、あなたは2つの観察をします:
- 宝物を見つけるのは難しい(長い)ので、そこにたどり着くには(潜在的に難しい)なぞなぞを解く必要があります。
- 宝物が見つかったら、入口に戻るのは簡単かもしれませんが、反対方向に同じパスを使用するだけです(ただし、パスを呼び出すには少しメモリが必要です)。
ダンジョンに入ると、ここに小さなノートがあります。(新しい部屋に入るとき)なぞなぞを解いた後に出るすべての部屋を書き留めるためにそれを使用することを決定します。これにより、入り口に戻ることができます。それは天才的な考えです、あなたはあなたの戦略を実装するのに1セントも費やしません。
あなたはダンジョンに入り、最初の1001個のなぞなぞを大成功で解決しましたが、ここでは、あなたが計画していなかった何かがあり、借りたノートにはスペースが残っていません。ダンジョン内で永遠に失われるよりも宝物を好まないので、クエストを放棄することを決定します(実際にスマートに見えます)。
再帰的なプログラムの実行
基本的に、それは宝物を見つけることとまったく同じです。ダンジョンはコンピュータのメモリです。あなたの目標は、宝物を見つけることではなく、いくつかの関数を計算することです(与えられたxに対してf(x)を見つけます)。表示は、単にf(x)を解くのに役立つサブルーチンです。あなたの戦略はコールスタック戦略と同じです、ノートブックはスタック、部屋は関数のリターンアドレスです:
x = ["over here", "am", "I"]
y = sorted(x) # You're about to enter a room named `sorted`, note down the current room address here so you can return back: 0x4004f4 (that room address looks weird)
# Seems like you went back from your quest using the return address 0x4004f4
# Let's see what you've collected
print(' '.join(y))
ダンジョンで発生した問題はここでも同じです。コールスタックのサイズは有限(ここでは1000)であるため、戻り値を返さずに関数を入力しすぎると、コールスタックがいっぱいになり、エラーが発生します。以下のように「親愛なる冒険家、私は非常に残念ですが、あなたのノートブックはいっぱいです」:RecursionError: maximum recursion depth exceeded
。呼び出しスタックを満たすために再帰は必要ありませんが、非再帰プログラムが戻ることなく1000関数を呼び出すことはほとんどありません。関数から戻ると、コールスタックは使用されたアドレスから解放されることを理解することも重要です(そのため、名前「スタック」。戻りアドレスは、関数に入る前にプッシュされ、戻るときに引き出されます)。単純な再帰の特別な場合(関数f
自分自身を1回呼び出す-何度も-)f
計算が完了するまで(宝物が見つかるまで)何度も繰り返し入力し、最初にf
呼び出した場所に戻るまで戻りますf
。呼び出しスタックは、すべての戻りアドレスから順番に解放されるまで、何も解放されません。
この問題を回避するには?
それは実際には非常に単純です。「それがどれだけ深くなることができるかわからない場合は、再帰を使用しないでください」。場合によっては、テールコールの再帰を最適化(TCO)できるため、これは常に正しいとは限りません。しかし、Pythonではそうではなく、「よく書かれた」再帰関数でさえスタックの使用を最適化しません。この質問について、Guidoからの興味深い投稿があります。TailRecursion Eliminationです。
再帰的な関数を反復的にするために使用できる手法があります。この手法は、独自のノートブックを持っていくことができます。たとえば、私たちが特定のケースで単にリストを探索している場合、部屋を入力することはサブリストを入力することと同じです。自問する必要があるのは、リストからその親リストに戻る方法です。答えはそれほど複雑ではありません。stack
空になるまで以下を繰り返します。
- 新しいサブリストを入力するときに、現在のリスト
address
をプッシュします(リストのアドレス+インデックスもアドレスであるため、コールスタックで使用されているのとまったく同じ手法を使用します)。index
stack
- アイテムが見つかるたび
yield
に(またはリストに追加します)、
- リストが完全に探索されたら、
stack
return address
(およびindex
)を使用して親リストに戻ります。
また、これはツリー内のDFSと同等であり、一部のノードはサブリストでA = [1, 2]
あり、一部は単純なアイテムです0, 1, 2, 3, 4
(の場合L = [0, [1,2], 3, 4]
)。ツリーは次のようになります。
L
|
-------------------
| | | |
0 --A-- 3 4
| |
1 2
DFSトラバーサルの事前注文は、L、0、A、1、2、3、4です。反復DFSを実装するには、スタックも「必要」であることに注意してください。以前に提案した実装では、次の状態になります(stack
およびflat_list
)。
init.: stack=[(L, 0)]
**0**: stack=[(L, 0)], flat_list=[0]
**A**: stack=[(L, 1), (A, 0)], flat_list=[0]
**1**: stack=[(L, 1), (A, 0)], flat_list=[0, 1]
**2**: stack=[(L, 1), (A, 1)], flat_list=[0, 1, 2]
**3**: stack=[(L, 2)], flat_list=[0, 1, 2, 3]
**3**: stack=[(L, 3)], flat_list=[0, 1, 2, 3, 4]
return: stack=[], flat_list=[0, 1, 2, 3, 4]
この例では、入力リスト(およびツリー)の深さが2であるため、スタックの最大サイズは2です。
実装
実装では、Pythonで単純なリストの代わりにイテレーターを使用することで、少し単純化できます。(サブ)イテレータへの参照は、(リストアドレスとインデックスの両方を持つ代わりに)サブリストの戻りアドレスを格納するために使用されます。これは大きな違いではありませんが、これは読みやすくなっています(そして少し高速でもあります)。
def flatten(iterable):
return list(items_from(iterable))
def items_from(iterable):
cursor_stack = [iter(iterable)]
while cursor_stack:
sub_iterable = cursor_stack[-1]
try:
item = next(sub_iterable)
except StopIteration: # post-order
cursor_stack.pop()
continue
if is_list_like(item): # pre-order
cursor_stack.append(iter(item))
elif item is not None:
yield item # in-order
def is_list_like(item):
return isinstance(item, list)
また、より多くの入力タイプを処理するように変更できるがあることis_list_like
にも注意してisinstance(item, list)
ください。ここでは、(反復可能)が単なるリストである最も単純なバージョンが必要でした。しかし、それを行うこともできます:
def is_list_like(item):
try:
iter(item)
return not isinstance(item, str) # strings are not lists (hmm...)
except TypeError:
return False
これは文字列を「単純なアイテム」と見なすため、ではなくflatten_iter([["test", "a"], "b])
が返されます。その場合、各アイテムで2回呼び出されることに注意してください。読者がこれをよりクリーンにすることが課題であるとしましょう。["test", "a", "b"]
["t", "e", "s", "t", "a", "b"]
iter(item)
他の実装のテストと備考
最後に、内部で()への再帰呼び出しを使用するため、を使用L
して無限にネストされたリストを出力することはできません。同じ理由で、関与の解決策は同じエラーメッセージで失敗します。print(L)
__repr__
RecursionError: maximum recursion depth exceeded while getting the repr of an object
flatten
str
ソリューションをテストする必要がある場合は、この関数を使用して単純なネストされたリストを生成できます。
def build_deep_list(depth):
"""Returns a list of the form $l_{depth} = [depth-1, l_{depth-1}]$
with $depth > 1$ and $l_0 = [0]$.
"""
sub_list = [0]
for d in range(1, depth):
sub_list = [d, sub_list]
return sub_list
これにより、build_deep_list(5)
>>> が得られます[4, [3, [2, [1, [0]]]]]
。