🦄

イテレーターパターンを学ぶ

に公開

イテレーターパターンを学ぶ

Pythonプログラミングに慣れてくると、forループを使ってリストなどの要素を一つずつ取り出す、という操作は日常的に行いますよね。

my_list = [1, 2, 3]
for item in my_list:
    print(item)

では、このfor ... in ...というシンプルな構文が、どのような仕組みで動いているか考えたことはありますか?その答えの核心にあるのが、今回学ぶイテレーターパターンです。

イテレーターパターンは、GoF(Gang of Four)によって定義された伝統的なデザインパターンのひとつで、「集合体の要素に順番にアクセスする方法を、その内部表現を公開することなく提供する」ものです。

この記事では、イテレーターパターンの基本的な概念から、なぜそれが重要なのか、そしてPythonでどのように実装するのかを、具体的なサンプルコードを通して一歩ずつ学んでいきます。forループの裏側を探る旅に出かけましょう!

イテレーターとは

イテレーター(Iterator)は、集合体の要素に順番にアクセスするための統一的な方法(インターフェース)を提供するオブジェクトです。Pythonでは、forループで使えるオブジェクトの裏側では、すべてこのイテレーターが活躍しています。

Pythonでオブジェクトをイテレーターとして振る舞わせるには、以下の二つの特別なメソッドを実装します。

  • __iter__(): イテレーターオブジェクト自身を返します。forループの開始時などに呼び出されます。
  • __next__(): 次の要素を返します。要素がなくなったら StopIteration という例外を発生させて、ループの終了を伝えます。

イテレーターパターンのメリット:なぜ内部を隠蔽すべきか

このパターンの最大のメリットは、集合体の内部実装と、それを利用する側のコード(クライアント)を分離できる点にあります。

例えば、本の集まりを管理する BookShelf というクラスを考えます。最初は内部を単純なリストで実装したとします。
もしクライアントコードが「BookShelf の内部はリストである」という前提で、インデックスを使って要素にアクセスしていると、どうなるでしょうか。

# 悪い例:内部のリストに直接アクセスしている
for i in range(len(my_book_shelf.books)):
    print(my_book_shelf.books[i].get_name())

将来、パフォーマンス向上のために BookShelf の内部実装をリストから辞書に変更したくなった場合、上記コードは動かなくなります。BookShelf を使っているすべてのクライアントコードを修正する必要に迫られ、非常に手間がかかり、バグの原因にもなります。

イテレーターパターンを使えば、クライアントは for book in my_book_shelf: というシンプルな形で要素にアクセスできます。BookShelf の内部がリストであろうと辞書であろうと、クライアント側のコードは一切変更する必要がありません。これにより、仕様変更に強く、保守性の高いコードを実現できるのです。


サンプルコード

それでは、実際に BookShelf を例に、イテレーターパターンの実装を見ていきましょう。
まずは登場人物となる Book クラスです。

Book.py
class Book:
    """本を表すクラス。"""
    def __init__(self, name: str):
        self.name = name

    def get_name(self) -> str:
        return self.name

パターン1: 抽象基底クラスを継承する厳格な実装

Pythonの collections.abc モジュールには、イテレーターが満たすべきインターフェースを定めた抽象基底クラス(ABC)があります。これらを継承することで、より厳格にパターンを実装できます。

  • Iterable: __iter__ メソッドを持つことを示す。BookShelf がこれを継承します。
  • Iterator: __iter____next__ を持つことを示す。BookShelfIterator がこれを継承します。
BookShelf_abc.py
from collections.abc import Iterator, Iterable
from Book import Book

class BookShelfIterator(Iterator):
    def __init__(self, book_shelf: 'BookShelf'):
        self._book_shelf = book_shelf
        self._index = 0

    def __next__(self) -> Book:
        if self._index < self._book_shelf.get_length():
            book = self._book_shelf.get_book_at(self._index)
            self._index += 1
            return book
        else:
            raise StopIteration

class BookShelf(Iterable):
    def __init__(self):
        self._books = []

    def get_book_at(self, index: int) -> Book:
        return self._books[index]
    
    def append_book(self, book: Book):
        self._books.append(book)

    def get_length(self) -> int:
        return len(self._books)
    
    def __iter__(self) -> BookShelfIterator:
        return BookShelfIterator(self)

パターン2: ダックタイピングによるPython的な実装

一方で、Pythonは「アヒルのように鳴き、アヒルのように歩くなら、それはアヒルだ」というダックタイピングの考え方が主流です。必ずしも抽象基底クラスを継承しなくても、必要なメソッド (__iter__, __next__) さえ実装されていれば、それはイテレーターとして扱われます。こちらのほうが、よりシンプルでPythonらしい書き方と言えるでしょう。

BookShelf_duck.py
from Book import Book

class BookShelfIterator:
    def __init__(self, book_shelf: 'BookShelf'):
        self._book_shelf = book_shelf
        self._index = 0

    def __iter__(self):
        return self
    
    def __next__(self) -> Book:
        if self._index < self._book_shelf.get_length():
            book = self._book_shelf.get_book_at(self._index)
            self._index += 1
            return book
        else:
            raise StopIteration
        
class BookShelf:
    def __init__(self):
        self._books = []

    def get_book_at(self, index: int) -> Book:
        return self._books[index]
    
    def append_book(self, book: Book):
        self._books.append(book)

    def get_length(self) -> int:
        return len(self._books)
    
    def __iter__(self):
        return BookShelfIterator(self)

クライアントコード

重要なのは、BookShelf がどちらのパターンで実装されていても、クライアント側のコードは全く同じになるという点です。

main.py
from Book import Book
# 実装に応じて BookShelf_abc や BookShelf_duck からインポート
from BookShelf_duck import BookShelf 

my_book_shelf = BookShelf()
my_book_shelf.append_book(Book("デザインパターン"))
my_book_shelf.append_book(Book("Clean Code"))

# イテレータを使っているので、内部構造を気にせずループできる
for book in my_book_shelf:
    print(book.get_name())

パターンの核心:「走査」と「操作」の分離

イテレーターパターンの本質を突き詰めると、それは**「走査(Traversal)」「操作(Operation)」**の責務を分離することにあります。

  • 走査: 集合体の要素をどのように巡るか、次の要素は何か、終わりはどこか、といったロジックです。これは イテレーターオブジェクト が担当します。
  • 操作: 走査によって取り出された個々の要素に対して、何をするかという処理です。これは クライアント(forループの中身) が担当します。

for book in my_book_shelf: というコードでは、in my_book_shelf の部分がイテレーターによる「走査」を、ループブロック内の print(book.get_name()) がクライアントによる「操作」を担っています。

分離がもたらすメリット

この責務の分離は、ソフトウェア設計にいくつかの大きな利点をもたらします。

1. 再利用性の向上

走査ロジックと操作ロジックをそれぞれ別の場所で再利用できます。

  • 多様な操作に同じ走査方法を適用: BookShelf を走査するイテレーターは一度作れば、「全書籍のタイトルを印刷する」「全書籍の価格を合計する」「在庫切れの書籍をリストアップする」といった様々な操作で再利用できます。
  • 多様な集合体に同じ操作を適用: 「タイトルを印刷する」という操作は、BookShelf から取り出した本にも、NewArrivals(新刊リスト)という別の集合体から取り出した本にも、同じように適用できます。
2. 多様な走査方法の提供

一つの集合体に対して、複数の異なる走査方法を簡単に提供できます。例えば、BookShelf に対して、以下のような異なるイテレーターを用意することが考えられます。

  • ForwardIterator: 通常通り、前から順番に走査する。
  • ReverseIterator: 後ろから逆順に走査する。
  • SaleItemIterator: セール対象の書籍のみを走査する。

クライアントは、使いたいイテレーターを選択するだけで、同じ操作ロジック(例:タイトルを印刷する)を異なる順序や条件で実行できます。

# 逆順イテレータを使うイメージ (BookShelfに reverse() メソッドがあると仮定)
for book in my_book_shelf.reverse():
    print(f"[逆順] {book.get_name()}")
3. 責務の明確化

それぞれのクラスが持つべき責任が明確になります(単一責任の原則)。

  • 集合体 (BookShelf): データを保持・管理することに専念する。
  • イテレーター (BookShelfIterator): 集合体を走査する方法を知っていることに専念する。
  • クライアント (main.py): 取り出した要素を使って何をしたいか、というビジネスロジックに専念する。

このように責務を分離することで、各コンポーネントがシンプルになり、コードの見通しが良く、メンテナンスしやすくなります。

発展: 内部実装の変更に対応する

ここで、BookShelf の内部実装をリストから辞書に変更してみましょう。イテレーターパターンがどのように機能するかを実感できるはずです。

BookShelf_dict.py
from Book import Book
from typing import Dict, Iterator

# イテレータは辞書の中身(Bookオブジェクト)を順に返すように修正
class BookShelfIterator:
    def __init__(self, books: Dict[str, Book]):
        self._iterator: Iterator[Book] = iter(books.values())

    def __iter__(self) -> Iterator[Book]:
        return self
    
    def __next__(self) -> Book:
        return next(self._iterator)

# BookShelfの内部実装をリストから辞書に変更
class BookShelf:
    def __init__(self):
        self._books: Dict[str, Book] = {}

    def append_book(self, book: Book):
        self._books[book.get_name()] = book

    def get_length(self) -> int:
        return len(self._books)
    
    def __iter__(self) -> BookShelfIterator:
        return BookShelfIterator(self._books)

BookShelfBookShelfIterator の実装は大きく変わりました。しかし、main.py のクライアントコードは 一切変更する必要がありません。これこそが、イテレーターパターンによる疎結合の力です。

もしイテレーターパターンを使わなかったら?

繰り返しになりますが、もしイテレーターパターンを使わず、クライアントが BookShelf の内部リストに直接アクセスしていた場合、この辞書への変更によってすべてのクライアントコードが動かなくなります。

my_book_shelf._books にアクセスしている箇所がプロジェクト内に100箇所あったら、その100箇所すべてを my_book_shelf._books.values() のように修正しなければなりません。これは多大な労力とバグのリスクを伴います。

まとめ

イテレーターパターンは、「走査」ロジックを集合体から分離し、統一的なアクセス方法を提供します。これにより、集合体の内部実装をカプセル化し、クライアントコードとの結合度を低く保ちます。結果として、走査方法と操作ロジックの再利用性が高まり、仕様変更に強く、保守性の高いコードを書くための強力な武器となるのです。

参考書籍:JAVA言語で学ぶデザインパターン入門 結城 浩

Discussion