この章では、より高度でPythonらしいコードを書くための強力な機能、ジェネレータとデコレータを学びます。これらの機能を使いこなすことで、メモリ効率の良いデータ処理を実装したり、既存の関数の振る舞いをエレガントに変更したりできるようになります。
Pythonのforループは非常にシンプルで強力ですが、その裏側ではイテレーションプロトコルという仕組みが動いています。これを理解することが、ジェネレータを学ぶ上での第一歩です。
forループで繰り返し処理が可能なオブジェクトのことです。リスト、タプル、辞書、文字列などがこれにあたります。内部に __iter__() メソッドを持つオブジェクトと定義されます。__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ループはこの例外を検知して、ループを自動的に終了してくれます。
イテレータを自作するには、クラスに __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]
巨大なファイルの各行を処理する場合など、ジェネレータ式は非常に有効です。
デコレータは、既存の関数のコードを一切変更せずに、その関数に新しい機能を追加(装飾)するための仕組みです。これは、関数を受け取って、新しい関数を返す高階関数として実装されます。
ログ出力、実行時間の計測、認証チェックなど、複数の関数に共通して適用したい「横断的な関心事」を扱うのに非常に便利です。
デコレータの基本的な構造は、関数を入れ子にすることです。
関数の実行前後にメッセージを表示する簡単なデコレータを見てみましょう。
>>> 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コードを書くための強力な武器となります。ぜひ積極的に活用してみてください。
はい、承知いたしました。先に作成したチュートリアルの末尾に追加する「この章のまとめ」と「練習問題」を作成します。
この章では、Pythonプログラミングをさらに高いレベルへ引き上げるための2つの強力な概念を学びました。
ジェネレータ: yieldキーワードを使うことで、メモリ効率に優れたイテレータを簡単に作成できることを学びました。ジェネレータ関数やジェネレータ式を使うことで、巨大なデータストリームや無限シーケンスを、必要な分だけ計算しながら扱うことができます。これは、パフォーマンスが重要なアプリケーションにおいて不可欠なテクニックです。
デコレータ: @シンタックスを用いることで、既存の関数のソースコードを変更することなく、機能を追加・変更できることを学びました。デコレータは、ロギング、実行時間計測、アクセス制御といった横断的な関心事を分離し、コードの再利用性を高め、DRY (Don't Repeat Yourself) の原則を維持するのに役立ちます。
これらの機能を使いこなすことは、単に高度な文法を覚えるだけでなく、Pythonの設計思想を理解し、より「Pythonらしい(Pythonic)」コードを書くための重要なステップです。
countdown(start) というジェネレータ関数を作成してください。この関数は、引数で与えられた start の数値から1まで、1ずつ減っていく数値を順番に yield します。例えば countdown(3) は、3, 2, 1 の順に値を生成します。
def countdown(start):
# 動作確認
cd_gen = countdown(5)
for i in cd_gen:
print(i)python practice9_1.py(出力例) 5 4 3 2 1
関数の実行時間を計測し、"実行時間: X.XXXX秒" のように表示するデコレータ @measure_time を作成してください。このデコレータを、少し時間のかかる処理を行う関数に適用して、動作を確認してみましょう。
ヒント: 時間の計測には time モジュールが使えます。処理の開始前と終了後で time.time() を呼び出し、その差分を計算します。
import time
def measure_time(func):
# 動作確認用の時間のかかる関数
@measure_time
def slow_function(n):
print(f"{n}まで数えます...")
time.sleep(n) # n秒間処理を停止
print("完了!")
slow_function(2)python practice9_2.py(出力例) 2まで数えます... 完了! 実行時間: 2.0021秒