基礎から学ぶPython入門 90日コース | 実践Python - Day 25:ジェネレータ

90日で身につけるPython
スポンサーリンク
スポンサーリンク

Day 25:ジェネレータで「必要な分だけ順番に出す」感覚を身につける

Day 25では、 イテレータ・yield・ジェネレータ を通して、 「全部を一気に作るのではなく、必要になったタイミングで1つずつ値を出す」 という考え方を身につけていきます。

イテレータとは何かをイメージする

「順番に取り出せるもの」というざっくりしたイメージ

Pythonでは、for 文で回せるもの(リスト・タプル・文字列など)は、 「順番に要素を取り出せる」という性質を持っています。 この「順番に取り出せるもの」を、より厳密には イテレータ(iterator) と呼びます。

  • イテレータは「次の値」を順番に返していく仕組みです。
  • for 文は、内部でイテレータを使って「次の値」を取り出しています。

リストは「イテラブル(iterable)」であり、 iter() を使うと「イテレータ」に変換できます。

numbers = [1, 2, 3]

it = iter(numbers)  # イテレータを取得します。

print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
# これ以上要素がない状態で next(it) を呼ぶと、StopIteration 例外が発生します。
Python
  • iter(リスト) で「イテレータ」を作る
  • next(イテレータ) で「次の値」を1つずつ取り出す

という流れが、for 文の裏側で起きています。

ジェネレータとは何か:イテレータを「自分で作る」ための仕組み

ジェネレータ=「自作イテレータ」

ジェネレータ(generator) は、 「自分でイテレータを作るための、Pythonの特別な仕組み」です。

  • 関数の形で書ける
  • yield を使って「次の値」を順番に返す
  • for 文で自然に回せる

という特徴があります。

イメージとしては、

  • 「値を1つずつ返していく関数」
  • 「呼ばれるたびに、前回の続きから動き出す関数」

です。

yield:ジェネレータの「心臓部」

returnとの違いを理解する

通常の関数は、return を使って値を返します。

def make_list():
    return [1, 2, 3]

result = make_list()
print(result)  # [1, 2, 3]
Python

一方、ジェネレータ関数では yield を使います。

def gen_numbers():
    yield 1
    yield 2
    yield 3

g = gen_numbers()
print(g)  # <generator object gen_numbers at ...>

for n in g:
    print(n)
Python

出力:

1
2
3

ここでの重要ポイントは、

  • yield は「ここで一旦値を返し、次に呼ばれたときはこの続きから再開する」という動きをすること
  • gen_numbers() を呼ぶと「リスト」ではなく「ジェネレータオブジェクト」が返ってくること
  • for 文で回すと、yield で返された値が順番に取り出されること

です。

ジェネレータの基本形をステップバイステップで理解する

シンプルなジェネレータ関数

まずは、1〜5までの数字を順番に返すジェネレータを書いてみます。

def count_up_to_5():
    """1〜5までの数字を順番に返すジェネレータです。"""
    print("ジェネレータ開始")
    yield 1
    print("1を返しました")
    yield 2
    print("2を返しました")
    yield 3
    print("3を返しました")
    yield 4
    print("4を返しました")
    yield 5
    print("5を返しました")
    print("ジェネレータ終了")
Python

使ってみます。

g = count_up_to_5()

for n in g:
    print("受け取った値:", n)
Python

実行イメージ:

ジェネレータ開始
受け取った値: 1
1を返しました
受け取った値: 2
2を返しました
受け取った値: 3
3を返しました
受け取った値: 4
4を返しました
受け取った値: 5
5を返しました
ジェネレータ終了

ここでのポイントは、

  • ジェネレータ関数は「最初に呼ばれたときに全部実行される」のではなく、 yield ごとに「少しずつ」実行されること
  • for 文が内部で next() を呼び、yield に到達するたびに値を受け取っていること

です。

ジェネレータの強み:大きなデータを「少しずつ」扱う

リストで一気に作る場合との比較

例えば、「1〜1000000までの数字の2乗」を扱いたいとします。

リストで一気に作る場合:

def make_squares_list(n):
    """1〜nまでの2乗をリストで返します。"""
    result = []
    for i in range(1, n + 1):
        result.append(i ** 2)
    return result

squares = make_squares_list(1_000_000)
print(len(squares))  # 1000000
Python

この場合、

  • 100万個の要素を一気にメモリに載せる
  • メモリ使用量が大きくなる

という問題があります。

ジェネレータで少しずつ返す場合:

def gen_squares(n):
    """1〜nまでの2乗をジェネレータで返します。"""
    for i in range(1, n + 1):
        # ここで1つずつ yield します。
        yield i ** 2

g = gen_squares(1_000_000)

# 最初の10個だけ使う
for i, value in enumerate(g, start=1):
    print(value)
    if i >= 10:
        break
Python

この場合、

  • 必要な分だけ順番に計算して返す
  • 「全部を一気にメモリに載せる」必要がない

というメリットがあります。

ジェネレータを使った「ちょっと実用的な」例

ファイルを1行ずつ読むジェネレータ

大きなテキストファイルを扱うとき、 「全部を一気に読み込む」のではなく「1行ずつ処理する」ほうが安全です。

def read_lines(path):
    """ファイルを1行ずつ読み込むジェネレータです。"""
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            # strip() で改行を取り除いてから返します。
            yield line.strip()
Python

使ってみます。

for line in read_lines("sample.txt"):
    print(line)
    # 必要なら途中で break してもOKです。
Python

ここでのポイントは、

  • with open(...) の中で for line in f: としていること
  • yield によって「1行ずつ」外に渡していること
  • 呼び出し側は「ジェネレータを for で回す」だけで、 大きなファイルでも安全に扱えること

です。

無限に続くジェネレータ(必要なところで止める)

ジェネレータは、「終わりのない列」を表現することもできます。

def infinite_counter(start=0):
    """startから始まる無限カウンタを返すジェネレータです。"""
    n = start
    while True:
        yield n
        n += 1
Python

使ってみます。

counter = infinite_counter(10)

for i, value in enumerate(counter, start=1):
    print(value)
    if i >= 5:
        break  # 無限ジェネレータなので、どこかで止める必要があります。
Python

出力:

10
11
12
13
14

ここでのポイントは、

  • ジェネレータは「無限列」も表現できること
  • 呼び出し側が「どこまで使うか」を決めることで、柔軟に扱えること

です。

ジェネレータ式:内包表記のように書ける形

リスト内包表記との比較

リスト内包表記:

squares_list = [n ** 2 for n in range(1, 6)]
print(squares_list)  # [1, 4, 9, 16, 25]
Python

ジェネレータ式:

squares_gen = (n ** 2 for n in range(1, 6))
print(squares_gen)  # <generator object ...>

for value in squares_gen:
    print(value)
Python
  • [] なら「リスト内包表記」
  • () なら「ジェネレータ式」

という違いがあります。

ジェネレータ式は、

  • 「全部を一気に作らず、必要な分だけ順番に取り出す」
  • 「メモリを節約できる」

というメリットがあります。

Day 25ミニテンプレート:ジェネレータ練習スクリプト

最後に、Day 25で学んだ内容をまとめて試せる「ジェネレータ練習スクリプト」のテンプレートを示します。

# day25_generators.py
# ジェネレータ入門の練習用スクリプト

def count_up_to_5():
    """1〜5までの数字を順番に返すジェネレータです。"""
    print("ジェネレータ開始")
    yield 1
    print("1を返しました")
    yield 2
    print("2を返しました")
    yield 3
    print("3を返しました")
    yield 4
    print("4を返しました")
    yield 5
    print("5を返しました")
    print("ジェネレータ終了")


def gen_squares(n):
    """1〜nまでの2乗をジェネレータで返します。"""
    for i in range(1, n + 1):
        yield i ** 2


def infinite_counter(start=0):
    """startから始まる無限カウンタを返すジェネレータです。"""
    n = start
    while True:
        yield n
        n += 1


def main():
    print("=== count_up_to_5 の例 ===")
    g = count_up_to_5()
    for value in g:
        print("受け取った値:", value)

    print("\n=== gen_squares の例(最初の10個だけ) ===")
    squares = gen_squares(1_000_000)
    for i, value in enumerate(squares, start=1):
        print(value)
        if i >= 10:
            break

    print("\n=== infinite_counter の例(start=10, 最初の5個) ===")
    counter = infinite_counter(10)
    for i, value in enumerate(counter, start=1):
        print(value)
        if i >= 5:
            break

    print("\n=== ジェネレータ式の例 ===")
    squares_gen = (n ** 2 for n in range(1, 6))
    for value in squares_gen:
        print(value)


if __name__ == "__main__":
    main()
Python

Day 25のまとめ

Day 25では、ジェネレータ入門として、

  • イテレータ=「次の値を順番に返す仕組み」というイメージ
  • yield を使って「少しずつ値を返す関数」を作る方法
  • 大きなデータや無限列を「必要な分だけ順番に処理する」考え方
  • ファイル読み込みや数列生成など、実用的な場面でのジェネレータの使い方
  • ジェネレータ式で「内包表記のように書ける形」を体験すること

をステップバイステップで学んでいただきました。

ここまで来ると、「全部を一気に作るのではなく、必要な分だけ順番に出す」という発想が少し育っているはずです。 この感覚は、今後の実践Python編で、大きなデータやストリーム処理を扱うときに、メモリを節約しつつ柔軟にコードを書くための大きな武器になっていきます。

タイトルとURLをコピーしました