{}const=>[]async()letfn</>var
開発Python

Pythonのジェネレーターとイテレーター:メモリを節約し、パフォーマンスを向上させる

この記事では、Pythonのジェネレーターとイテレーターを使用して大量のデータを効率的に処理する方法について説明します。ファイル処理、API、データパイプラインの実例。メモリをオーバーフローさせることなく、あらゆる量の情報にスケールできるコードを書く方法を学びます。

К

Kodik

著者

2分で読める

数ギガバイトのログファイルを処理する必要があるとします。最も簡単な解決策は、ファイル全体をメモリにロードし、行に分割して処理を開始することです。しかし、RAMが不足している場合はどうなりますか?または、そのようなファイルが数十個ある場合はどうしますか?このような状況のために、Pythonにはジェネレーターとイテレーターがあります。これは、大量のデータをメモリに完全にロードすることなく、大量のデータを処理できる強力なツールです。

イテレーターとは何ですか?

イテレーターは、コレクション全体を一度にメモリにロードすることなく、コレクションの要素を1つずつ反復することを可能にするオブジェクトです。Python では、__iter__() および __next__() メソッドを実装するオブジェクトはすべてイテレーターです。

for item in collectionを書くと、Pythonは暗黙的にiter(collection)を呼び出してイテレーターを取得し、次に例外StopIterationがスローされるまで、次の要素を取得するためにnext()を繰り返し呼び出します。

# シンプルなイテレーターの例
class CountDown:
    def __init__(self, start):
        self.current = start
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

# 使用
counter = CountDown(5)
for num in counter:
    print(num)  # 表示: 5、4、3、2、1

🔥 10万人以上の学生が参加中

理論を読むのに疲れた?
コーディングの時間だ!

Kodik — 実践でプログラミングを学ぶアプリ。AIメンター、インタラクティブなレッスン、実際のプロジェクト。

🤖 AI 24時間
🎓 修了証
💰 無料
🚀 始める
今日参加

ジェネレーター:ステロイドのイテレーター

ジェネレーターは、イテレーターを作成する簡単な方法です。メソッド __iter__()__next__() を持つクラスを書く代わりに、単にキーワード yield を持つ関数を作成します。ジェネレーターは、呼び出し間の状態を自動的に保存し、停止した場所から実行を再開します。

def count_down(start):
    while start > 0:
        yield start
        start -= 1

# 使用
for num in count_down(5):
    print(num)  # 表示: 5、4、3、2、1

利点は明らかです。コードはよりコンパクトでわかりやすくなり、機能はそのままです。

なぜメモリを節約できるのですか?

ジェネレーターの主な利点は、遅延計算です。要素は必要なときにのみ作成され、すぐに処理され、忘れられる可能性があります。具体的な例を見てみましょう。

# 悪い:すべてをメモリにロードする
def read_large_file_bad(file_path):
    with open(file_path, 'r') as file:
        lines = file.readlines()  # ファイル全体がメモリにあります!
    return [line.strip().upper() for line in lines]

# 良い:行ごとに処理します
def read_large_file_good(file_path):
    with open(file_path, 'r') as file:
        for line in file:  # ファイルはすでにイテレーターです!
            yield line.strip().upper()

# 使用
for processed_line in read_large_file_good('huge_log.txt'):
    # 1行ずつ処理します
    process(processed_line)

最初の場合、ファイルのサイズが2GBの場合、プログラムは少なくとも2GBのRAMを使用します。2番目の場合、ファイルサイズに関係なく、わずか数キロバイトです。

実際の使用例。

API からのデータ処理

APIはページごとにデータを返すことがよくあります。ジェネレーターはこの複雑さを隠すことができます。

def fetch_all_users(api_client, page_size=100):
    page = 1
    while True:
        users = api_client.get_users(page=page, size=page_size)
        if not users:
            break
        for user in users:
            yield user
        page += 1

# 使用
for user in fetch_all_users(api):
    print(user['name'])  # ユーザーを1人ずつ処理します

無限シーケンス

ジェネレーターを使用すると、メモリがいっぱいになるリスクなしに無限のシーケンスを作成できます。

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 必要なだけ要素を取ることができます
from itertools import islice
first_ten = list(islice(fibonacci(), 10))
print(first_ten)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

データ処理パイプライン

ジェネレーターは、処理チェーンの作成に最適です。

def read_csv(file_path):
    with open(file_path) as f:
        for line in f:
            yield line.strip().split(',')

def filter_by_status(rows, status):
    for row in rows:
        if row[2] == status:  # 3 列目のステータス
            yield row

def extract_emails(rows):
    for row in rows:
        yield row[1]  # 2 列目のメール

# コンベアを組み立てる
rows = read_csv('users.csv')
active_users = filter_by_status(rows, 'active')
emails = extract_emails(active_users)

# ここからが本当の処理の始まりです
for email in emails:
    send_notification(email)

このアプローチの素晴らしい点は、コンベアの各ステージが独立してテストされ、メモリが1つの要素にのみ使用されることです。

ジェネレーター式

ジェネレータ関数に加えて、Pythonはジェネレータ式をサポートしています。これは、ジェネレータを作成するためのコンパクトな構文です。

# リスト理解 - メモリ内にリストを作成します
squares_list = [x**2 for x in range(1000000)]  # メモリを大量に消費する

# ジェネレーター式 — ジェネレーターを作成します
squares_gen = (x**2 for x in range(1000000))  # ほとんどメモリを使用しません

# 関数で使用できます
total = sum(x**2 for x in range(1000000))  # 効果的です!

角括弧の代わりに丸括弧が使用されていることに注意してください。これは、メモリを節約するためにコードに加えることができる最も簡単で効果的な変更の1つです。

発電機を使用すべきでない場合

すべての利点にもかかわらず、発電機は常に適しているとは限りません。

データへの複数アクセスが必要な場合: ジェネレーターは1回のみ実行できます。データに複数回アクセスする必要がある場合は、リストを使用することをお勧めします。

アクセス速度が重要な場合: リストからインデックスで要素を取得するのは瞬時であり、ジェネレーターは最初から並べ替える必要があります。

データがメモリに格納されるタイミング: データセットが小さい場合、ジェネレーターを作成するためのオーバーヘッドは不当なものになる可能性があります。

長さを知る必要がある場合: len()はジェネレーターでは動作せず、長さの計算には完全な反復が必要です。

高度なテクニック。

ジェネレーターの委任

yield from を使用して、実行を別のジェネレーターに委任できます。

def read_multiple_files(*file_paths):
    for path in file_paths:
        with open(path) as f:
            yield from f  # 行ごとの読み取りを委任する

# 複数のファイルを1つのストリームとして読み込む
for line in read_multiple_files('log1.txt', 'log2.txt', 'log3.txt'):
    process(line)

発電機との双方向通信

ジェネレーターは、yieldを介して値を返すだけでなく、send()メソッドを介して値を受け取ることもできます。

def running_average():
    total = 0
    count = 0
    average = None
    while True:
        value = yield average
        total += value
        count += 1
        average = total / count

# 使用
avg = running_average()
next(avg)  # 発電機を起動します
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
print(avg.send(30))  # 20.0

効率を測定する

メモリ使用量の実際の違いを見てみましょう。

import sys

# リスト
numbers_list = [x for x in range(1000000)]
print(f"List: {sys.getsizeof(numbers_list) / 1024 / 1024:.2f} MB")

# 発電機
numbers_gen = (x for x in range(1000000))
print(f"Generator: {sys.getsizeof(numbers_gen) / 1024:.2f} KB")

私のマシンでは、結果は千倍の違いを示しています。リストは約8 MBを占め、ジェネレーターは0.1 KB未満です。

結論

ジェネレーターとイテレーターは、単なる構文上の便利な機能ではなく、大量のデータを扱うための基本的なツールです。ジェネレーターとイテレーターを使用すると、あらゆるサイズのデータにスケールする、より効率的なコードを書くことができます。ジェネレーターをいつ、どのように使用するかを理解することは、経験豊富な開発者と初心者の違いです。

シンプルなものから始めましょう。データへの複数アクセスを必要としない場合は、リスト理解をジェネレーター式に置き換えます。次に、ファイルを処理するためのジェネレーター関数を書いてみてください。そして、徐々に、メモリ内のコレクションではなく、データフローについて考えることに気づくでしょう。

アプリケーション コディック Python、JavaScript、HTML、CSS、その他多くのテクノロジーをカバーする、初心者向けの構造化コースを提供しています。

私たちの Telegramチャンネル、コミュニティサポート、追加資料、経験豊富な開発者からの質問への回答が見つかります。

効率よく楽しくプログラミングを学びましょう!

🎯先延ばしをやめよう

記事は気に入った?
実践の時間だ!

Kodikでは読むだけでなく、すぐにコードを書く。理論 + 実践 = 本当のスキル。

即座に実践
🧠AIがコードを説明
🏆修了証

登録不要 • カード不要