Pythonのリストのメモリ効率を最適化する方法(generatorの活用)
生徒
「Pythonで大量のデータを扱うとき、メモリがいっぱいになることがあるって聞いたんですが、本当ですか?」
先生
「はい、Pythonでリストを使って何百万件ものデータを一度に保存しようとすると、パソコンのメモリを圧迫してしまうことがあります。」
生徒
「そんなとき、どうすればメモリを節約できるんですか?」
先生
「Pythonにはジェネレーター(generator)という仕組みがあって、これを使うとメモリ効率を大きく改善できます。詳しく説明していきますね。」
1. Pythonのリストは便利だけどメモリをたくさん使う
Pythonのlist(リスト)は、データを順番にたくさん保存できる便利な仕組みです。たとえば、数字の一覧を保存して、後でまとめて使いたいときに便利です。しかし、リストはすべてのデータを最初からまとめてメモリに保存するため、データの量が増えると、メモリを大量に消費してしまいます。
2. ジェネレーターとは何か?
ジェネレーターとは、必要なときに1つずつ値を取り出せる仕組みのことです。つまり、必要なタイミングで1個ずつ作るイメージです。全部のデータを一気に作るわけではないので、メモリの使用量が少なくて済みます。
例えば、パン屋さんで100個のパンを一気に焼くとオーブンも大変ですが、注文が入ったら1個ずつ焼く仕組みにすれば、省エネになります。Pythonのジェネレーターもそれと似た考え方です。
3. range()とジェネレーターの違いを理解しよう
Pythonのrange()関数は、実はジェネレーターに似た動きをします。例えば、以下のように使うと100万個の数を扱うことができますが、これはジェネレーターとして動くので、メモリに全て保存していないのです。
for i in range(1000000):
print(i)
4. ジェネレーター関数を自分で作ってみよう
Pythonではyieldというキーワードを使って、自分でジェネレーター関数を作ることができます。下の例は、0から9までの数を順番に出力するジェネレーター関数です。
def number_generator():
for i in range(10):
yield i
for num in number_generator():
print(num)
5. リスト内包表記との違いに注意しよう
Pythonのリスト内包表記では、以下のようにすべての値を一気にメモリに作ります。
squares = [i * i for i in range(1000000)]
一方、ジェネレーター式は丸カッコ()を使って、必要なときに1つずつ作る形になります。
squares = (i * i for i in range(1000000))
このように、大きなデータを扱うときはジェネレーター式を使うことで、パソコンのメモリを効率よく使うことができます。
6. 実際にメモリ使用量を比べてみよう
Pythonのsys.getsizeof()を使えば、オブジェクトが使っているメモリの大きさを調べることができます。
import sys
list_data = [i for i in range(1000)]
gen_data = (i for i in range(1000))
print(sys.getsizeof(list_data))
print(sys.getsizeof(gen_data))
このように、リストは大きなメモリサイズになりますが、ジェネレーターは非常に小さなサイズです。
9024
112
7. ジェネレーターを使うときの注意点
ジェネレーターは一度しか使えないという特徴があります。リストのように何度も繰り返して使いたい場合には、都度再生成が必要です。
また、ジェネレーターは中身を一覧表示したり、個数を調べたりするのが難しいため、用途に応じてリストとジェネレーターを使い分けることが大切です。
8. Python初心者がジェネレーターを使うべき場面とは?
以下のようなときは、ぜひジェネレーターの活用を検討してください。
- データの量が多くてメモリが心配なとき
- 1回だけ順番にデータを処理すればよいとき
- ファイルを1行ずつ読みながら処理したいとき
たとえば、大きなCSVファイルを開いて、1行ずつ処理するようなときは、リストではなくジェネレーターを使うとメモリ効率が大幅に向上します。
まとめ
リストとジェネレーターを正しく理解することの重要性
ここまで、Pythonにおけるリストとジェネレーターの違い、そしてメモリ効率を最適化するための考え方について詳しく学んできました。Pythonのリストは直感的で扱いやすく、初心者にとっても理解しやすいデータ構造ですが、その反面、すべての要素を一度にメモリへ展開するという特徴があります。そのため、大量データ処理や長時間動作するプログラムでは、メモリ不足やパフォーマンス低下の原因になることがあります。
一方で、ジェネレーターは「必要な分だけ値を生成する」という遅延評価の仕組みを持っています。この特性により、Pythonで大量のデータを扱う場合でも、メモリ使用量を抑えながら安全に処理を進めることができます。今回の記事で紹介した yield を使ったジェネレーター関数や、ジェネレーター式は、Pythonのメモリ効率を考えるうえで非常に重要な知識です。
ジェネレーターを使った最適化の考え方
Pythonでの最適化というと、処理速度ばかりに目が向きがちですが、実務や学習を進めていくと「メモリ効率」を意識する場面が必ず出てきます。特に、CSVファイルやログファイル、APIレスポンスのようにデータ量が事前に読めない場合には、ジェネレーターの活用が大きな効果を発揮します。
例えば、以下のようにジェネレーター関数を使えば、大量のデータを1件ずつ安全に処理できます。この記事内で使った構文や書き方と同じ形式で確認してみましょう。
def data_generator():
for i in range(1000000):
yield i
for value in data_generator():
print(value)
このような書き方を身につけることで、Pythonのプログラムはより堅牢になり、メモリ消費を抑えた設計ができるようになります。Python初心者のうちからジェネレーターの考え方に触れておくことは、今後の学習や実務において大きな財産になります。
用途に応じた使い分けが理解度を高める
ただし、すべてをジェネレーターにすればよいわけではありません。リストは再利用しやすく、要素数の取得やインデックスアクセスが簡単という強みがあります。そのため、小規模なデータ処理や、複数回同じデータを使う処理では、リストの方が適しているケースも多くあります。
重要なのは、「データ量」「処理回数」「メモリ使用量」を意識しながら、リストとジェネレーターを使い分けることです。Pythonの基礎文法としてだけでなく、実践的なプログラミングスキルとして、この判断力を身につけていきましょう。
生徒
「今回の記事を読んで、Pythonのリストが便利な反面、メモリをたくさん使う理由がよく分かりました。ジェネレーターって、ただ難しそうな仕組みだと思っていたんですが、実はすごく実用的なんですね。」
先生
「その気づきはとても大切です。Pythonでは、書けることと、適切に書くことは別です。ジェネレーターは、メモリ効率を意識したプログラムを書くための基本的な考え方のひとつですよ。」
生徒
「リスト内包表記とジェネレーター式の違いも理解できました。カッコの違いだけで、こんなにメモリの使い方が変わるのは驚きです。」
先生
「そうですね。Pythonは読みやすさが重視される言語ですが、その裏でどんな動きをしているのかを知ることで、より一段階上のプログラミングができるようになります。今回学んだジェネレーターの知識は、今後のPython学習でも必ず役立ちますよ。」
生徒
「これからは、データ量が多い処理ではジェネレーターを意識して使ってみます。Pythonのメモリ効率を考えながらコードを書く習慣を身につけたいです。」