見出し画像

Python 3: Deep Dive (Part 2 - Iterators, Generators): yield from(セクション6-2/14)

  • ジェネレータを使用してカードデッキのコードを書き直すことで、より読みやすく効率的な実装が可能になり、特に遅延評価の利点を活用できることを説明しています。

  • ジェネレータ式はリスト内包表記と比べてメモリ使用量が少なく、大規模データセットの処理に適していますが、全要素の処理時間は同等であることを解説しています。

  • `yield from`文を使用することで、ネストされたイテレータの処理を簡潔に記述でき、特にファイル連結などの実践的なケースでコードの可読性とメモリ効率が向上することを示しています。

このブログ記事では、「Python 3 Deep Dive (Part 2 - イテレータ、ジェネレータ)」コースのセクション6のレッスン64から68までを中心に、Pythonジェネレータの探求を続けていきます。ジェネレータの実践的な使用例を深く掘り下げ、ジェネレータ式のパフォーマンスへの影響を理解し、ネストされたイテレータを扱う際に`yield from`文がどのようにコードを簡略化できるかを発見していきます。


ジェネレータを使用したカードデッキの書き直し

レッスン64では、以前作成したカードデッキの例を再検討します。今回は、カスタムイテレータの代わりにジェネレータを使用してデッキを再構築し、コードを簡略化し、遅延評価のためのジェネレータの力を活用します。

元のアプローチ

以前は、インデックス計算を使用してデッキを生成していました:

from collections import namedtuple

Card = namedtuple('Card', 'rank suit')
SUITS = ('Spades', 'Hearts', 'Diamonds', 'Clubs')
RANKS = tuple(range(2, 11)) + tuple('JQKA')

def card_gen():
    for i in range(len(SUITS) * len(RANKS)):
        suit = SUITS[i // len(RANKS)]
        rank = RANKS[i % len(RANKS)]
        yield Card(rank, suit)

このコードは、除算と剰余演算を使用してスートとランクのインデックスを計算しており、扱いにくい場合があります。

簡略化されたジェネレータアプローチ

ネストされたループを使用してジェネレータ関数を簡略化できます:

def card_gen():
    for suit in SUITS:
        for rank in RANKS:
            yield Card(rank, suit)

このバージョンはより読みやすく、インデックス計算が不要になります。以下のようにジェネレータを反復処理できます:

for card in card_gen():
    print(card)

反復可能なカードデッキの作成

カードデッキを反復可能にするために、ジェネレータをクラス内にカプセル化できます:

class CardDeck:
    SUITS = ('Spades', 'Hearts', 'Diamonds', 'Clubs')
    RANKS = tuple(range(2, 11)) + tuple('JQKA')
    
    def __iter__(self):
        return CardDeck.card_gen()
    
    @staticmethod
    def card_gen():
        for suit in CardDeck.SUITS:
            for rank in CardDeck.RANKS:
                yield Card(rank, suit)

これで、`CardDeck`のインスタンスを作成し、それを反復処理できます:

deck = CardDeck()
cards = [card for card in deck]

ジェネレータを使用しているため、デッキは消耗せずに複数回反復処理できます。

逆順反復のサポートの追加

逆順反復をサポートするために、`reversed`メソッドを実装できます:

class CardDeck:
    # ... (前のコード)
    
    def __reversed__(self):
        return CardDeck.reversed_card_gen()
    
    @staticmethod
    def reversed_card_gen():
        for suit in reversed(CardDeck.SUITS):
            for rank in reversed(CardDeck.RANKS):
                yield Card(rank, suit)

これで、デッキを逆順に反復処理できます:

rev_deck = reversed(CardDeck())
cards_reversed = [card for card in rev_deck]

ジェネレータ式とパフォーマンス

レッスン65と66では、ジェネレータ式とリスト内包表記と比較した場合のパフォーマンスへの影響について探求します。

内包表記の構文

リスト内包表記

リスト内包表記は、リストを作成する簡潔な方法を提供します:

squares = [i ** 2 for i in range(5)]

以下をサポートします:

リスト内包表記は即時評価され、すべての要素が即座に計算されメモリに格納されます。

ジェネレータ式とリスト内包表記の比較

ジェネレータ式は、角括弧の代わりに丸括弧を使用する以外は、リスト内包表記と同じ構文を使用します:

squares_gen = (i ** 2 for i in range(5))

主な違い

  • 遅延評価:ジェネレータは要求されたときに値を計算し、前もって計算しません。

  • メモリ効率:すべての値を一度にメモリに格納しません。

  • イテレータ:ジェネレータはイテレータであり、一度だけ反復処理できます。

# リスト内包表記
squares_list = [i ** 2 for i in range(5)]
print(squares_list)
# 出力: [0, 1, 4, 9, 16]

# ジェネレータ式
squares_gen = (i ** 2 for i in range(5))
print(list(squares_gen))
# 出力: [0, 1, 4, 9, 16]

`squares_gen`を消費した後は、消耗しているため再度反復処理することはできません。

ネストされた内包表記

ジェネレータ式は、リスト内包表記と同様にネストすることもできます。

掛け算表の例

リスト内包表記を使用

start, stop = 1, 10
mult_table = [[i * j for j in range(start, stop + 1)] for i in range(start, stop + 1)]

ジェネレータ式を使用

mult_table_gen = ((i * j for j in range(start, stop + 1)) for i in range(start, stop + 1))

ジェネレータからテーブルを完全に具現化するには、反復処理する必要があります:

table = [list(row) for row in mult_table_gen]

パフォーマンスの考慮事項

時間パフォーマンス

  • リスト内包表記:すべての要素を即座に計算し、大きなデータセットの場合は時間がかかる可能性があります。

  • ジェネレータ式:必要になるまで要素を計算せずに、即座にジェネレータを作成します。

すべての要素を反復処理する場合、最終的に計算が発生するため、両方のアプローチの合計実行時間は同様です。

メモリ使用量

ジェネレータは、すべての要素をメモリに格納しないため、大幅にメモリ効率が良くなります。これは、大きなデータセットやストリームを扱う際に理想的です。

例:パスカルの三角形のタイミング計測

両方の方法を使用してパスカルの三角形を生成する際の時間とメモリ使用量を比較できます。

リスト内包表記アプローチ

def pascal_list(size):
    return [[combo(n, k) for k in range(n + 1)] for n in range(size + 1)]

ジェネレータ式アプローチ

def pascal_gen(size):
    return ((combo(n, k) for k in range(n + 1)) for n in range(size + 1))

タイミング結果

  • 作成時間:ジェネレータの方が作成が速い。

  • 反復時間:すべての要素を反復処理する場合は両者とも同様。

  • メモリ使用量:ジェネレータは大幅に少ないメモリを使用。


`yield from`を使用した反復の委譲

レッスン67と68では、ジェネレータがその操作の一部を別のジェネレータに委譲することを可能にする`yield from`文について学びます。

ネストされた反復の簡略化

ネストされたイテレータを扱う場合、多くの場合ネストされたループを書く必要があります:

def matrix_iterator(n):
    for row in matrix(n):
        for item in row:
            yield item

`yield from`を使用してこれを簡略化できます:

def matrix_iterator(n):
    for row in matrix(n):
        yield from row

これにより内部ループが置き換えられ、コードがより簡潔になります。

実践例:ファイルの連結

自動車ブランド名が含まれる複数のファイルがあり、それらを順番に読み取りたいとします。

従来のアプローチ

brands = []

with open('car-brands-1.txt') as f:
    for brand in f:
        brands.append(brand.strip('\n'))

with open('car-brands-2.txt') as f:
    for brand in f:
        brands.append(brand.strip('\n'))

with open('car-brands-3.txt') as f:
    for brand in f:
        brands.append(brand.strip('\n'))

for brand in brands:
    print(brand)

このアプローチは、すべてのデータをメモリに読み込み、同様のコードブロックを繰り返します。

ジェネレータと`yield from`を使用

まず、ファイルからデータをクリーンアップして生成するジェネレータを作成します:

def gen_clean_read(file):
    with open(file) as f:
        for line in f:
            yield line.strip('\n')

次に、ファイルを連結するジェネレータを作成します:

def brands(*files):
    for file in files:
        yield from gen_clean_read(file)

これで、すべてのデータをメモリに読み込むことなく、すべてのブランドを反復処理できます:

files = ('car-brands-1.txt', 'car-brands-2.txt', 'car-brands-3.txt')

for brand in brands(*files):
    print(brand)

`yield from`文は反復を`gen_clean_read`ジェネレータに委譲し、コードを簡略化してメモリ効率を改善します。


結論

レッスン64から68では、ジェネレータがPythonのコードをどのように簡略化し最適化できるかを探求しました:

  • カードデッキの書き直し:ジェネレータを使用することで、カードデッキの実装がより読みやすく効率的になり、順方向と逆方向の反復をサポートしています。

  • ジェネレータ式:ジェネレータ式とリスト内包表記の違い、特に遅延評価とメモリ使用に関する違いを学びました。

  • パフォーマンス:すべての要素を反復処理する場合、ジェネレータは必ずしも時間的なパフォーマンスの利点を提供するわけではありませんが、大きなデータセットを扱う場合や、すべてのデータが必要ない場合に大きなメモリの節約と効率性を提供します。

  • `yield from`文:この強力な機能により、ネストされたイテレータを扱う際にサブジェネレータに反復を委譲し、コードを簡略化できます。

ジェネレータを組み込み、その動作を理解することで、特に反復と大規模なデータ処理を扱う際に、より効率的で読みやすく、保守しやすいPythonコードを書くことができます。


参考文献とリソース

プログラミングを楽しみましょう!


「超本当にドラゴン」へ

いいなと思ったら応援しよう!