読者です 読者をやめる 読者になる 読者になる

Iteratorパターン


Rubyによるデザインパターン』(ラス・オルセン著 ピアソン・エデュケーション刊)の例をPythonに変換して書いています。


目次



一般にプログラミング言語は複数のデータ構造を持ち(リスト・連想配列・集合など)異なった方法で値のオブジェクトを保持しています。親オブジェクトが内部でどのように子オブジェクトを保持しているかに関わらず、子オブジェクトに順番にアクセスする方法をもたらすのが、Iteratorパターンです。


とはいえ、PythonではIteratorパターンについては、大して書くことが無いのですが・・・

primes = [2, 3, 5, 7, 11, 13]
for i in primes:
    print i


Python組み込みのlist, string, dict 等には、既にイテレータの機能が備わっています。
だから、どれも for 文で値(stringだと文字、dictだとkey)を順に取ってくる事が出来ます。

自分で定義したclassをfor文で使えるようにするには、__iter__メソッドを定義します。


__iter__メソッドの定義方法は2種類あります。
1つ目はイテレータオブジェクトを返す方法です。

#すみません、非実際的な例です。
class CompositeTask(Task):
    def __iter__(self):
        return IterCompositeTask(self) #イテレータオブジェクトを返します。

class IterCompositeTask(object): #CompositeTask用のイテレータオブジェクトです。
    def __init__(self, compositeTask):
        self.compositeTask = compositeTask
        self.index = 0
    
    def __iter__(self): #イテレータオブジェクトにも__iter__メソッドが必要です。
        return self     #必ず自分自身を返します。
    
    def next(self): #次の値を返すメソッド
        if self.index < len(self.compositeTask.sub_tasks):
            i = self.index
            self.index += 1
            return self.compositeTask.sub_tasks[i] 
        else:
            raise StopIteration #次の値が無いときは、StopIteration例外を投げます。


2つ目はジェネレータ関数として定義する方法、下の例がそれです。
yield文で値を次々と返します。

class CompositeTask(Task):
    def __iter__(self): #for文で使うためには、__iter__を定義します
        for sub_task in self.sub_tasks:
            yield sub_task #この値がfor文で使われます


こうすると、自動的にイテレータオブジェクトが作られます。

こちらの方法の方が簡単で分かりやすいと思います。

また、iter関数で、イテレータオブジェクトを取り出す事ができます。

it = iter([0, 2, 4, 6, 8])
print it
while True:
     print it.next()
#実行結果
#
#0
#2
#4
#6
#8
#Traceback (most recent call last):
#  File "<stdin>", line 2, in <module>
#StopIteration



わざわざiter関数を使う場合としては、
例えば、リストにキーと値が交互に入っていて、それを元に辞書を作る関数。

def seq_to_dict(aseq):
    """
    >>> seq_to_dict([1, "one", 2, "two", 3, "three"])
    {1: 'one', 2: 'two', 3: 'three'}
    """
    it = iter(aseq)
    d = {}
    while True:
        try:
            key = it.next()
            value = it.next()
            d[key] = value
        except StopIteration:
            break
    return d

def seq_to_dict2(aseq):
    """
    本当はこう書いたほうが性能は良いはず。
    >>> seq_to_dict2([1, "one", 2, "two", 3, "three"])
    {1: 'one', 2: 'two', 3: 'three'}
    """
    from itertools import tee, islice, izip
    a, b = tee(aseq)
    return dict(izip(islice(a, 0, None, 2), islice(b, 1, None, 2)))


itertoolsモジュールには、イテレータを加工する関数が10数個定義されて、
seq_to_dictの様にiter関数を取り回すより、
itertoolsを利用した方が、一般に性能は良くなるはずです。


もっとも、多用すると、わけが分からなくなりますが。