見出し画像

Python 3: Deep Dive (Part 2 - Iterators, Generators): カスタムイテラブル(セクション4-2/14)

  • Pythonのイテラブルとイテレータを分離することで、効率的な反復処理が可能になる

  • イテレーションプロトコル(`iter()`と`next()`)の理解が、カスタムイテラブルとイテレータの作成に重要。

  • イテレータの手動消費により、ファイル処理などの実際のアプリケーションで柔軟性と制御が向上する。

「Python 3:ディープダイブ(パート2 - イテレータ、ジェネレータ)」のUdemyコースを進める中で、Pythonがイテレーションをより深いレベルでどのように処理するかを理解する重要な点に到達しました。セクション4のレッスン38から40は、イテラブルをイテレータから分離すること、イテレータを手動で消費すること、そしてこれらの概念の実践的な応用に焦点を当てています。このブログ記事では、これらのレッションを解説し、コード例を提供して理解を深めます。

はじめに

以前、Pythonにおけるイテラブルとイテレータの仕組みを探求し、イテレータプロトコルの実装やカスタムイテレータオブジェクトの作成について学びました。しかし、イテレータオブジェクトがコレクションデータも維持していた場合、イテレータの枯渇や、オブジェクト全体を再作成せずにイテレーションを再開できないなどの制限に直面しました。

レッスン38から40では、これらの課題に取り組むため、コレクションとイテレータを分離し、イテレータを手動で消費する方法を探ります。この分離により、基礎となるデータ構造を再構築することなく、コレクションを複数回イテレートできるようになり、コードの効率性と柔軟性が向上します。

レッスン38:コレクションとイテレータの分離

コレクションとイテレータを組み合わせた場合の問題点

オブジェクトがコレクションとイテレータの両方の役割を果たす場合、1回の完全なイテレーション後に枯渇してしまいます。再度イテレートするには、オブジェクト全体を再作成する必要があり、特にコレクションが大きかったり構築コストが高い場合には非効率的です。

解決策:関心の分離

この問題を克服するために、コレクション(イテラブル)とイテレータを分離します:

  • コレクション(イテラブル):データを維持し、新しいイテレータを作成するメソッドを提供します。

  • イテレータ:イテレーションのロジックを処理し、現在の位置を追跡し、`next()`メソッドを実装します。

コレクションとイテレータの実装

コレクションクラス

class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin', 'Rome', 'Madrid', 'London']

    def __len__(self):
        return len(self._cities)

    def __iter__(self):
        return CityIterator(self)
  • `iter()`メソッド:新しいイテレータのインスタンスを返し、コレクションに対する複数の独立したイテレーションを可能にします。

イテレータクラス

class CityIterator:
    def __init__(self, city_obj):
        self._city_obj = city_obj
        self._index = 0

    def __iter__(self):
        return self  # イテレータは自身を返す

    def __next__(self):
        if self._index >= len(self._city_obj):
            raise StopIteration
        else:
            item = self._city_obj._cities[self._index]
            self._index += 1
            return item
  • `next()`メソッド:次の項目を返し、コレクションが枯渇した時に`StopIteration`を発生させるロジックを実装します。

イテラブルとイテレータの使用

cities = Cities()

for city in cities:
    print(city)

出力:

Paris
Berlin
Rome
Madrid
London
  • `cities`をイテレートするたびに、`iter()`メソッドが新しい`CityIterator`を作成し、コレクションを再構築することなく複数回のイテレーションを可能にします。

重要な観察点

  • イテラブルとイテレータは別物:コレクション(`Cities`)とイテレータ(`CityIterator`)は異なるオブジェクトです。

  • イテレータは消費可能:イテレータが枯渇すると、再開できません。しかし、イテラブルは新しいイテレータを作成できます。

  • イテラブルは`iter()`を実装する:毎回新しいイテレータを返します。

レッスン39:イテレータとイテラブルのコーディング例

イテレーションの流れの理解

print文を追加することで、`iter()`と`next()`がいつ呼び出されるかを観察できます:

print文を追加したイテレータ

class CityIterator:
    def __init__(self, city_obj):
        print('CityIterator __init__ called')
        self._city_obj = city_obj
        self._index = 0

    def __iter__(self):
        print('CityIterator __iter__ called')
        return self

    def __next__(self):
        print('CityIterator __next__ called')
        if self._index >= len(self._city_obj):
            raise StopIteration
        else:
            item = self._city_obj._cities[self._index]
            self._index += 1
            return item

citiesのイテレーション

cities = Cities()

for city in cities:
    print(city)

出力

CityIterator __init__ called
CityIterator __next__ called
Paris
CityIterator __next__ called
Berlin
CityIterator __next__ called
Rome
CityIterator __next__ called
Madrid
CityIterator __next__ called
London
CityIterator __next__ called
  • 流れ

    • ループが始まると`Cities.iter()`が呼び出され、新しい`CityIterator`が作成されます。

    • イテレータの`next()`メソッドが`StopIteration`が発生するまで繰り返し呼び出されます。

イテラブルとシーケンスの混在

オブジェクトは`iter()`に加えて`getitem()`を実装することで、イテラブルとシーケンスの両方になることができます:

`Cities`に`__getitem__()`を追加

class Cities:
    def __init__(self):
        self._cities = ['Paris', 'Berlin', 'Rome', 'Madrid', 'London']

    def __len__(self):
        return len(self._cities)

    def __getitem__(self, s):
        print('Cities __getitem__ called')
        return self._cities[s]

    def __iter__(self):
        return CityIterator(self)

インデックスによるアクセス

cities = Cities()
print(cities[0])

出力:

Cities __getitem__ called
Paris
  • Pythonはイテレーションには`iter()`を、インデックス付けには`getitem()`を使用します。

組み込みのイテラブルとイテレータ

  • リスト、辞書、セットなどはすべてイテラブルです。

  • これらは`iter()`を実装し、時には`getitem()`も実装しています。

  • Pythonのイテレーションプロトコルは、カスタムタイプと組み込みタイプで一貫しています。

レッスン40:イテレータの手動消費

手動消費が有用な場合

イテレータの手動消費は、特定の要素をスキップしたり、イテレーション中の例外を処理したりするなど、イテレーションプロセスを正確に制御する必要がある場合に有用です。

例:CSVファイルの処理

以下のようなCSVファイルがあるとします:

  • 1行目にヘッダーが含まれています。

  • 2行目にデータ型が含まれています。

  • 残りの行にデータ行が含まれています。

ファイルの手動読み込み

with open('cars.csv') as file:
    file_iter = iter(file)
    headers = next(file_iter).strip('\n').split(';')
    data_types = next(file_iter).strip('\n').split(';')
    for line in file_iter:
        data = line.strip('\n').split(';')
        # データの処理
  • 手動イテレーション:

    • `next(file_iter)`を使用して特定の行を消費します。

    • ループでファイルの残りをイテレートします。

データ型の動的キャスト

キャスト関数の定義

def cast(data_type, value):
    if data_type == 'DOUBLE':
        return float(value)
    elif data_type == 'INT':
        return int(value)
    else:
        return str(value)

各データ行のキャスト

def cast_row(data_types, data_row):
    return [cast(dt, val) for dt, val in zip(data_types, data_row)]

データ行の処理

from collections import namedtuple

with open('cars.csv') as file:
    file_iter = iter(file)
    headers = next(file_iter).strip('\n').split(';')
    data_types = next(file_iter).strip('\n').split(';')
    Car = namedtuple('Car', headers)
    cars = [
        Car(*cast_row(data_types, line.strip('\n').split(';')))
        for line in file_iter
    ]
  • 結果:正しい型のフィールドを持つ`Car`名前付きタプルのリスト。

イテレータの手動消費の利点

  • 効率性:ファイル全体をメモリに読み込むのを避けられます。

  • 柔軟性:特定の行のカスタム処理が可能です。

  • 制御:イテレーションプロセスの正確な制御が可能です。

結論

レッスン38から40では、Pythonにおけるイテラブルとイテレータについての理解を深めました:

  • コレクションとイテレータの分離:冗長なデータ作成を避けることで効率性を高め、同じコレクションに対する複数のイテレーションを可能にします。

  • イテレーションプロトコルの理解:`iter()`と`next()`がどのように協調して働くかを知ることは、カスタムイテラブルとイテレータを作成する上で重要です。

  • イテレータの手動消費:イテレーションプロセスの制御を提供し、ファイル処理のような実世界のアプリケーションで有用です。

これらの概念を習得することで、Pythonでより複雑なデータ構造とイテレーションパターンを扱う準備が整います。今後のレッスンでは、イテレータを基盤とし、さらに強力なデータ管理とイテレーションのツールを提供するジェネレータについて探究します。


注:コード例は説明用です。完全な実装とコンテキストについては、セクション4のレッスン38から40のコース資料を参照してください。


「超本当にドラゴン」へ

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