はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第18回目です。
今回は、集合体の内部構造を公開することなく、その要素を順番に走査するためのIterator(イテレーター)パターンについて解説します。
Iteratorパターンとは?
Iteratorパターンは、コレクション(リスト、配列など)の内部表現に依存せずに、その要素にアクセスするための振る舞いパターンです。これにより、クライアントはコレクションがどのようにデータを格納しているかを知る必要がなくなり、コードの柔軟性が高まります。
例えるなら、音楽再生リストです。
あなたは、プレイリストがどのように曲を保存しているか(配列か、リンクリストか)を知る必要はありません。ただ「次の曲へ」というボタンを押すだけで、次の曲にアクセスできます。Iteratorパターンは、この「次の要素へ」という共通の操作を提供します。
このパターンの主な目的は以下の通りです。
- カプセル化: 集合体の内部構造をクライアントから隠蔽する
- 統一的な走査: 異なる種類の集合体(リスト、ツリーなど)に対して、同じインターフェースで走査できるようにする
- 複数の走査: 同じ集合体に対して、複数のイテレーターを同時に動作させることができる
Javaでの実装:厳格なインターフェース
Javaは、java.util.Iterator
という標準インターフェースを言語に組み込んでいます。拡張for文(for-each)は、このインターフェースを内部的に利用しています。
このパターンでは、以下のコンポーネントが登場します。
-
イテレーターインターフェース(Iterator):
hasNext()
(次に要素があるか)とnext()
(次の要素を取得)メソッドを定義します - 具象イテレータークラス(ConcreteIterator): イテレーターインターフェースを実装し、特定の集合体を走査します
-
集合体インターフェース(Aggregate): イテレーターを生成する
iterator()
メソッドを定義します - 具象集合体クラス(ConcreteAggregate): 集合体インターフェースを実装し、具象イテレーターのインスタンスを返します
以下に、簡単なリストを走査する例を示します。
// JavaでのIteratorパターンの実装例
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.NoSuchElementException;
// 1. 集合体インターフェース
interface Aggregate<T> {
Iterator<T> iterator();
}
// 2. 具象集合体クラス
class MyList<T> implements Aggregate<T>, Iterable<T> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
public int size() {
return items.size();
}
@Override
public Iterator<T> iterator() {
return new MyListIterator();
}
// 3. 具象イテレータークラス(内部クラスとして実装)
private class MyListIterator implements Iterator<T> {
private int position = 0;
@Override
public boolean hasNext() {
return position < items.size();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
T item = items.get(position);
position++;
return item;
}
@Override
public void remove() {
if (position <= 0) {
throw new IllegalStateException();
}
items.remove(--position);
}
}
}
// 使い方
public class IteratorExample {
public static void main(String[] args) {
MyList<String> fruits = new MyList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// 明示的なイテレーター使用
System.out.println("明示的なイテレーター:");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// 拡張for文(内部的にイテレーターを使用)
System.out.println("\n拡張for文:");
for (String fruit : fruits) {
System.out.println(fruit);
}
// 複数のイテレーターの同時使用例
System.out.println("\n複数イテレーター:");
Iterator<String> iter1 = fruits.iterator();
Iterator<String> iter2 = fruits.iterator();
while (iter1.hasNext() && iter2.hasNext()) {
System.out.printf("%s - %s%n", iter1.next(), iter2.next());
}
}
}
Javaでは、イテレーターを明示的に作成し、hasNext()
とnext()
メソッドを使ってコレクションを走査します。また、Iterable
インターフェースを実装することで、拡張for文でも使用できるようになります。
Pythonでの実装:言語機能としてのイテレータ
Pythonは、イテレーターパターンを言語の根本的な部分に組み込んでいます。for
ループは、オブジェクトの__iter__()
メソッド(イテレーターを返す)と、イテレーターの__next__()
メソッド(次の要素を返す)を自動的に呼び出します。また、ジェネレーターは、イテレーターを非常に簡潔に作成するための強力な機能です。
以下に、Pythonでイテレーターを実装する例を示します。
# PythonでのIteratorパターンの実装例
# 1. 具象イテレータークラス
class MyListIterator:
def __init__(self, collection):
self._collection = collection
self._position = 0
def __iter__(self):
return self
def __next__(self):
if self._position >= len(self._collection):
raise StopIteration
item = self._collection[self._position]
self._position += 1
return item
# 2. 具象集合体クラス(__iter__メソッドを持つ)
class MyList:
def __init__(self, items=None):
self._items = items or []
def add(self, item):
self._items.append(item)
def __len__(self):
return len(self._items)
def __iter__(self):
return MyListIterator(self._items)
# 使い方
def main():
fruits = MyList(["Apple", "Banana", "Cherry"])
# for文が自動的にイテレーターを呼び出す
print("基本的な反復:")
for fruit in fruits:
print(fruit)
# 明示的なイテレーター使用
print("\n明示的なイテレーター:")
iterator = iter(fruits)
try:
while True:
print(next(iterator))
except StopIteration:
pass
# 複数のイテレーターの同時使用
print("\n複数イテレーター:")
iter1 = iter(fruits)
iter2 = iter(fruits)
try:
while True:
fruit1 = next(iter1)
fruit2 = next(iter2)
print(f"{fruit1} - {fruit2}")
except StopIteration:
pass
if __name__ == "__main__":
main()
この例では、for
ループがMyList
オブジェクトの__iter__
を呼び出し、返されたイテレーターオブジェクトの__next__
メソッドを繰り返し呼び出しています。
ジェネレーターを使ったよりPythonicな実装
Pythonでは、ジェネレーターを使うことで、さらに簡潔にイテレーターを実装できます。yield
キーワードを持つ関数は、自動的にイテレーターを返します。
# ジェネレーターを使ったIteratorパターンの実装例
class MyList:
def __init__(self, items=None):
self._items = items or []
def add(self, item):
self._items.append(item)
def __iter__(self):
"""ジェネレーターを使った簡潔な実装"""
for item in self._items:
yield item
def reverse_iter(self):
"""逆順のイテレーター(ジェネレーター使用)"""
for i in range(len(self._items) - 1, -1, -1):
yield self._items[i]
# 使い方
fruits = MyList(["Apple", "Banana", "Cherry"])
print("通常順序:")
for fruit in fruits:
print(fruit)
print("\n逆順序:")
for fruit in fruits.reverse_iter():
print(fruit)
# リスト内包表記との組み合わせ
uppercase_fruits = [fruit.upper() for fruit in fruits]
print(f"\n大文字変換: {uppercase_fruits}")
このジェネレーターによる実装は、イテレーターの状態管理(position
など)をPythonが自動的に行ってくれるため、開発者の負担を大幅に減らします。
実際の使用例:異なるデータ構造での統一的なアクセス
Iteratorパターンの真価は、異なるデータ構造に対して統一的なインターフェースを提供することにあります。
Java例:ツリー構造のイテレーター
// ツリー構造のイテレーター例
class TreeNode<T> {
T data;
List<TreeNode<T>> children = new ArrayList<>();
TreeNode(T data) {
this.data = data;
}
void addChild(TreeNode<T> child) {
children.add(child);
}
// 深さ優先探索のイテレーター
Iterator<T> depthFirstIterator() {
return new Iterator<T>() {
private Stack<TreeNode<T>> stack = new Stack<>();
{
stack.push(TreeNode.this);
}
@Override
public boolean hasNext() {
return !stack.isEmpty();
}
@Override
public T next() {
if (!hasNext()) throw new NoSuchElementException();
TreeNode<T> current = stack.pop();
// 子要素を逆順でスタックに追加(左から右の順序を保つため)
for (int i = current.children.size() - 1; i >= 0; i--) {
stack.push(current.children.get(i));
}
return current.data;
}
};
}
}
Python例:ジェネレーターを使ったツリー走査
class TreeNode:
def __init__(self, data):
self.data = data
self.children = []
def add_child(self, child):
self.children.append(child)
def depth_first_iter(self):
"""深さ優先探索のジェネレーター"""
yield self.data
for child in self.children:
yield from child.depth_first_iter()
def breadth_first_iter(self):
"""幅優先探索のジェネレーター"""
from collections import deque
queue = deque([self])
while queue:
node = queue.popleft()
yield node.data
queue.extend(node.children)
パフォーマンスと注意点
メモリ効率性
Iteratorパターンの大きな利点の一つは、**遅延評価(lazy evaluation)**です。特にPythonのジェネレーターは、必要な時にのみ値を生成するため、大きなデータセットでもメモリ効率的に処理できます。
def fibonacci_generator(n):
"""フィボナッチ数列をジェネレーターで生成"""
a, b = 0, 1
count = 0
while count < n:
yield a
a, b = b, a + b
count += 1
# メモリ効率的:一度に全ての値を生成しない
for num in fibonacci_generator(1000000):
if num > 1000:
break
print(num)
注意点
- 並行変更の問題: イテレーション中にコレクションが変更されると、予期しない動作が発生する可能性があります
- 一方向性: 基本的なイテレーターは前の要素に戻ることができません
- 状態管理: 複数のイテレーターが同じコレクションを走査する際の状態管理に注意が必要です
まとめ:本質は「透過的なアクセス」
特性 | Java | Python |
---|---|---|
主な解決策 |
Iterator インターフェースの実装 |
__iter__ と__next__ メソッド、またはジェネレーター |
設計思想 | 厳格なインターフェースによるカプセル化 | 言語機能による透過的なイテレーション |
コードの量 | 比較的多い(明示的なクラス定義が必要) | 非常に簡潔(特にジェネレーター) |
柔軟性 | 型安全性が高く、明示的な制御が可能 | 動的で表現力豊か、遅延評価が容易 |
パフォーマンス | コンパイル時最適化の恩恵 | インタープリター言語だが、ジェネレーターは効率的 |
Iteratorパターンは、両言語で実装のスタイルは異なりますが、**「内部構造を隠蔽し、要素に順次アクセスする」**という本質は共通です。Javaは明示的なインターフェースでこれを実現し、Pythonは言語の強力な機能(イテレータープロトコル、ジェネレーター)を介して、よりシンプルかつ自然に同じ目的を達成します。
どちらの言語でも、Iteratorパターンは大規模なデータ処理、異なるデータ構造への統一的なアクセス、メモリ効率的な処理において重要な役割を果たします。
次回は、オブジェクト間の複雑なやり取りを仲介するMediatorパターンについて解説します。お楽しみに!
次回のテーマは、「Day 19 Mediatorパターン:複雑な相互作用を仲介する」です。