第9章: さらに先へ:ジェネレータとデコレータ

この章では、より高度でPythonらしいコードを書くための強力な機能、ジェネレータデコレータを学びます。これらの機能を使いこなすことで、メモリ効率の良いデータ処理を実装したり、既存の関数の振る舞いをエレガントに変更したりできるようになります。

イテレータとイテラブル

Pythonのforループは非常にシンプルで強力ですが、その裏側ではイテレーションプロトコルという仕組みが動いています。これを理解することが、ジェネレータを学ぶ上での第一歩です。

  • イテラブル (Iterable): forループで繰り返し処理が可能なオブジェクトのことです。リスト、タプル、辞書、文字列などがこれにあたります。内部に __iter__() メソッドを持つオブジェクトと定義されます。
  • イテレータ (Iterator): 「次の値」を返す __next__() メソッドを持ち、値を一つずつ取り出すためのオブジェクトです。イテレータは一度最後まで進むと、それ以上値を取り出すことはできません。

forループは、まずイテラブルオブジェクトの __iter__() を呼び出してイテレータを取得し、次にそのイテレータの __next__() を繰り返し呼び出して要素を一つずつ取り出しています。

REPLで動きを見てみましょう。iter()関数でイテレータを取得し、next()関数で要素を取り出します。

>>> my_list = [1, 2, 3] >>> my_iterator = iter(my_list) >>> type(my_iterator) <class 'list_iterator'> >>> next(my_iterator) 1 >>> next(my_iterator) 2 >>> next(my_iterator) 3 >>> next(my_iterator) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

最後のnext()呼び出しで StopIteration という例外が発生しているのがわかります。forループはこの例外を検知して、ループを自動的に終了してくれます。

ジェネレータ関数とyieldキーワード

イテレータを自作するには、クラスに __iter__()__next__() を実装する必要がありますが、少し手間がかかります。そこで登場するのがジェネレータです。ジェネレータは、イテレータを簡単に作成するための特別な関数です。

ジェネレータ関数は、通常の関数と似ていますが、値を返すのにreturnの代わりにyieldを使います。

  • yieldの働き: yieldは値を返すだけでなく、その時点で関数の実行を一時停止し、関数の状態(ローカル変数など)を保存します。次にnext()が呼ばれると、停止した場所から処理を再開します。

これにより、巨大なデータセットを扱う際に、全てのデータを一度にメモリに読み込む必要がなくなります。必要な時に必要な分だけデータを生成するため、非常にメモリ効率が良いコードが書けます。

フィボナッチ数列を生成するジェネレータの例を見てみましょう。

>>> def fib_generator(n): ... a, b = 0, 1 ... count = 0 ... while count < n: ... yield a ... a, b = b, a + b ... count += 1 ... >>> f = fib_generator(5) >>> type(f) <class 'generator'> >>> next(f) 0 >>> next(f) 1 >>> next(f) 1 >>> next(f) 2 >>> next(f) 3 >>> next(f) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration # ジェネレータはもちろんforループで使うことができます >>> for num in fib_generator(8): ... print(num, end=' ') ... 0 1 1 2 3 5 8 13

ジェネレータ式

リスト内包表記に似た構文で、より簡潔にジェネレータを作成する方法がジェネレータ式です。リスト内包表記の []() に変えるだけで作れます。

リスト内包表記はリストオブジェクトを生成するため、要素数が多いとメモリを大量に消費します。一方、ジェネレータ式はジェネレータオブジェクトを返すため、遅延評価(必要になるまで計算しない)が行われ、メモリ使用量を抑えられます。

# リスト内包表記 >>> list_comp = [i * 2 for i in range(5)] >>> list_comp [0, 2, 4, 6, 8] >>> type(list_comp) <class 'list'> # ジェネレータ式 >>> gen_exp = (i * 2 for i in range(5)) >>> gen_exp <generator object <genexpr> at 0x...> >>> type(gen_exp) <class 'generator'> >>> next(gen_exp) 0 >>> next(gen_exp) 2 >>> list(gen_exp) # 残りの要素をリストに変換 [4, 6, 8]

巨大なファイルの各行を処理する場合など、ジェネレータ式は非常に有効です。

デコレータの概念と基本的な作り方

デコレータは、既存の関数のコードを一切変更せずに、その関数に新しい機能を追加(装飾)するための仕組みです。これは、関数を受け取って、新しい関数を返す高階関数として実装されます。

ログ出力、実行時間の計測、認証チェックなど、複数の関数に共通して適用したい「横断的な関心事」を扱うのに非常に便利です。

基本的な作り方

デコレータの基本的な構造は、関数を入れ子にすることです。

  1. 外側の関数(デコレータ関数)は、装飾したい対象の関数を引数として受け取ります。
  2. 内側の関数(ラッパー関数)で、受け取った関数を呼び出す前後に、追加したい処理を記述します。
  3. 外側の関数は、この内側の関数を返します。

関数の実行前後にメッセージを表示する簡単なデコレータを見てみましょう。

>>> def my_decorator(func): ... def wrapper(): ... print("--- 処理を開始します ---") ... func() ... print("--- 処理が完了しました ---") ... return wrapper ... >>> def say_hello(): ... print("こんにちは!") ... # デコレートされた新しい関数を作成 >>> decorated_hello = my_decorator(say_hello) >>> decorated_hello() --- 処理を開始します --- こんにちは! --- 処理が完了しました ---

この書き方をより簡単にするための構文が @(アットマーク)、シンタックスシュガーです。

>>> @my_decorator ... def say_goodbye(): ... print("さようなら!") ... >>> say_goodbye() --- 処理を開始します --- さようなら! --- 処理が完了しました ---

@my_decorator は、say_goodbye = my_decorator(say_goodbye) と同じ意味になります。こちらのほうが直感的で、Pythonのコードで広く使われています。

ジェネレータとデコレータは、最初は少し複雑に感じるかもしれませんが、使いこなせばよりクリーンで効率的なPythonコードを書くための強力な武器となります。ぜひ積極的に活用してみてください。