HN 日本語サマリー

← 一覧へ戻る
プログラミング

「for x in y」があなたから隠しているもの – From Scratch Code

What `for x in y` hides from you – From Scratch Code (fromscratchcode.com)

8 pointsby rbanffy5 コメント

要約

Pythonの`for x in y`構文は、リストや文字列などの要素を一つずつ取り出す便利な機能ですが、その背後ではイテレータプロトコルが使われています。Pythonは直接コレクションをループするのではなく、`iter()`でイテレータを取得し、`next()`で要素を取り出し、`StopIteration`でループを終了するという仕組みで動作しています。このイテレータの概念を理解することで、Pythonの多様なオブジェクトでのループやジェネレータの動作がより明確になります。

全文翻訳

「for x in y」があなたから隠しているもの Pythonの「for」ループが、手抜きをやめたときに実際に行っていること。 2026年7月8日 7分で読めます Pythonには、あまりにも簡単に使えるため、何もしていないかのように忘れてしまう機能がいくつかあります。例えば `for x in y` を考えてみましょう。リストや文字列、範囲を作成すると、Pythonは親切に一度に1つのアイテムを返してくれます。インデックス変数や境界チェックはありません。C/C++ の `i++` や JavaScript の `forEach` と比較すると、Python のバージョンは「ただ動く」のです。 長い間、私は `for x in y` を単に「このオブジェクトをループする」という意味の構文として扱い、それで十分でした。しかし、私のPythonインタープリタであるMemphisをRustで構築し始めたとき、ついに手抜きをやめて、非常に無礼な質問に答えなければならなくなりました。「forループは実際には何をしているのか?」 その答えは、予想よりもシンプルでありながら、より複雑であることがわかりました。 幻想 小さな例から始めましょう。 for x in [10, 20, 30]: print(x) これを実行すると、おなじみの動作が得られます:一度に1つの値が、順番に表示されます。今のところ、特に驚きはありません。 Pythonを始めたばかりの人にとって、次のようなメンタルモデルを持つのは簡単です: Python はリストを覗き込みます 要素を一つずつ見ていきます それぞれを x に代入します そしてループ本体を実行します これは行動としては間違っていませんが、最も重要な部分を隠しています:Python はコレクションを直接ループしているわけではありません。それはイテレータをループしているのです。 その区別は重要です。なぜなら、それが `for` がこれほど多くの異なる種類のオブジェクトで機能する理由と、Pythonでのイテレーションがこれほど柔軟に感じられる理由を説明するからです。 隠されたステップ Python がこれを見たとき: for x in [10, 20, 30]: print(x) 実際のストーリーは、次のようなものに近いです: it = iter([10, 20, 30]) while True: try: x = next(it) print(x) except StopIteration: break これが核心です。`for` ループは実際には次のことを行います: `iter(...)` を繰り返し呼び出してイテレータを要求する `next(...)` を呼び出して次の値を取得する `next(...)` が `StopIteration` を発生させたら停止する `for x in y` という構文は、単なるきれいなラッパーです。 これが私にとって理解できたとき、多くのPythonの動作が不思議に思えなくなりました。 それを直接確認できます インタープリタの内部に触れることなく、自分で証明できます。 nums = [10, 20, 30] it = iter(nums) print(next(it)) print(next(it)) print(next(it)) これはリストの要素を一度に一つずつ出力します。しかし、一歩踏み込みすぎると: nums = [10, 20, 30] it = iter(nums) print(next(it)) print(next(it)) print(next(it)) print(next(it)) 2番目のスニペットも実行してみてください。最後の `next(it)` は `StopIteration` を発生させます。これはループが終了したことを知る方法です。 したがって、ループは特別な方法で「リストから読み取っている」わけではありません。あなたが自分で使えるのと同じイテレータプロトコルを使用しています。 なぜこれが重要なのか 一見すると、これは技術的には正しいが特に有用ではない事実の一つであるように思えるかもしれません。私はそうは思いません。 この1つの考え方で、以下のすべてが機能する理由が説明できます: for x in [1, 2, 3]: print(x) for ch in "cat": print(ch) for n in range(3): print(n) これらはまったく異なる種類のオブジェクトです。リストは文字列ではなく、範囲でもありません。しかし、Pythonはそれらすべてをループできます。なぜなら、それぞれがイテレータを生成できるからです。 また、ジェネレータが `for` と自然に連携する理由も説明します。ジェネレータは、この同じプロトコルに参加する別のPythonタイプにすぎません。すぐにその例を見てみましょう。 一度これを見ると、Pythonのイテレーションモデルは魔法のように感じるのではなく、統一されているように感じ始めます。 私が実装しなければならなかった部分 Memphisでは、「何らかの方法でこれをループする」という言い訳はできませんでした。実行時に実際に何を行うかを決定する必要がありました。 大まかに言うと、私のツリーウォークインタープリタは、`for` ループを次のように処理します: 右辺の式を評価する 結果に対して `iter(...)` を呼び出す 繰り返し `next(...)` を呼び出す 返された各値をループ変数にバインドする `StopIteration` が発生したら停止する これを書き出すとほとんど退屈に聞こえますが、それはPython仕様への賛辞として言っています。優れた抽象化は、明確に名前を付けられるようになると、しばしば退屈になります。 私にとっての驚きは、`for` がイテレーションを使用することではありませんでした。それはループ自体がどれだけ知らないかということでした。 `for` ループには、リスト、タプル、範囲、文字列、ジェネレータ、その他の特別なロジックは必要ありません。イテレータプロトコルが必要なだけです。具体的なオブジェクトが詳細を処理します。 完全なPythonでは、これはさらに一歩進みます:カスタムオブジェクトは、`__iter__()` と `__next__()` を実装することで参加できます。私はまだMemphisでその部分に取り組んでいますが、この設計の最もクールな結果の一つです。 それは非常にPythonicな設計です。 この考え方をさらに理解するのに役立つ別の例を考えてみましょう: items = [1, 2, 3] it = iter(items) for x in it: print(x) for x in it: print("again:", x) 自分で実行してみてください。2番目のループは何も出力しません。なぜなら、イテレータは最初のループですでに使い果たされていたからです。`for` ループは何も「巻き戻し」ませんでした。単に `next(...)` を呼び出し続けて、何も残らなくなるまで続けただけです。 この区別をより具体的にしたい場合は、型も確認してください: items = [1, 2, 3] it = iter(items) print(type(items)) print(type(it)) これは微妙ですが重要な区別です: イテラブル(iterable)は通常、新しいイテレータを提供できます(リスト、文字列、範囲など)。 イテレータ(iterator)は通常、片道切符です。 ジェネレータ関数は、呼び出すたびに新しいジェネレータを作成できますが、ジェネレータオブジェクト自体はすでにイテレータです。 この違いは、`in` という単語の後ろに隠れています:イテレータをすでに渡していない限り、新しいイテレータを初期化します。 ここにもう一つ便利なバリエーションがあります: pairs = [(1, 10), (2, 20), (3, 30)] for x, y in pairs: print(x, y) これは異なる種類のループのように見えるかもしれませんが、実際にはそうではありません。イテレーション部分は同じです:Pythonはまだイテレータを要求し、一度に1つの値を取り出します。単に各値が2つの要素を持つタプルであり、Pythonがそのタプルを `x` と `y` にアンパックしているだけです。 したがって、`for x, y in z` は特別なフレーバーではなく、通常のイテレーションとアンパッキングです。 あなたの脳を壊すための例 これを定着させるには、ジェネレータで遊ぶと役立ちます。 def countdown(): print("Starting") yield 3 yield 2 yield 1 print("Done") for x in countdown(): print("Got", x) 実行して、順序を注意深く見てください。 ここで `for` ループが隠しているのは、単なる繰り返し以上のものです。`next(...)` の各呼び出しはジェネレータを再開し、次の `yield` まで実行し、その値をループに渡し、再び一時停止します。 一度イテレーションをこのように考え始めると、`for` ループは平坦に感じられなくなりました。それらは、時間とともに値を生成する方法を知っている他のオブジェクトとループとの間のプロトコルになりました。 それが、私がMemphisを構築する上でますます楽しんでいる、より正確なメンタルモデルです。Pythonには、使用を快適にするための滑らかな表面がたくさんあります。しかし、それをゼロから再実装するとき、私はその下のレバーを見ることができます。この場合、それは驚くほど小さなメカニズムでした: `iter(...)` `next(...)` `StopIteration` 終わり Pythonのループ構文の最も良い点は、初心者がこれらを理解する前に生産的になれることです。それは印象的な言語設計です。 しかし、ジェネレータ、カスタムイテラブル、または使い果たされたイテレータに関連するバグに遭遇すると、それらが何を隠しているのかを学ばなかった場合、より良い構文は負担になる可能性があります。 私が繰り返し使っているフレーズはこれです: `for x in y` は「y をループする」という意味ではありません。それは「y にイテレーションする方法を尋ねる」という意味です。 それは大したことではないように聞こえるかもしれませんが、私にとっては大きな変化でした。そして、もしあなたが `for x in y` をブラックボックスとしてほとんど使用してきたなら、それはあなたにも何かを変えるかもしれません。