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

何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『Python in Practice』を読む【3.4 Iterator Pattern】

先日、Amazonで注文した『Python in Practice』(PiP)がやっと届いた。これでも当初の予定よりも2週間ほど早い。

Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns (Developer's Library)

Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns (Developer's Library)

なお、電子版の方が圧倒的に安いしすぐ入手できるので紙媒体にこだわりがなければその方がよいと思う。
Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns (Developer's Library)

Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns (Developer's Library)

以前、「Pythonで頑張る『Java言語で学ぶデザインパターン入門』」と称してJavaで書かれたサンプルをPythonで書き直していたがはてなブックマークにてPython向けの本で学ばないのかという旨のコメントがあり、全く持ってその通りだと関心してしまった。気を取り直して「『Python in Practice』を読む」と称して頑張ってみたい。最初の3章はデザインパターンに関する話、4章が並列・分散処理、5章がC言語による拡張、6章がネットワーク、7, 8章がグラフィックに関する話である。差し当たり興味があるのは5章までであるのでそこまで読めればいいかなと思っている。
さて、PiPのデザインパターンGoFと同じ順番で出てくるので、わかりやすい順序で登場するという『Java言語で学ぶデザインパターン入門』の順序に従って読もうと思い、Iteratorパターンから始める。

GoFによると、「リストのような集約オブジェクトは、その内部構造を明らかにすることなく、要素にアクセスする方法をユーザに対して提供するべきである」(P275)とある。集約オブジェクトからアクセスや走査のための役割を取り出して扱うとよいとある。Pythonでは言語自体がIteratorパターンをサポートしている。PiPによると3つの方法がある:

  1. Sequence Protocol Iterators __getitem__ を実装する方法
  2. Two-Argment form of built-in iter() function 組み込み函数iter()に2つの引数を与える方法
  3. Iterator protocol Iterators __iter__ を実装する方法

まずはSequence Protocol Iteratorsである。以下はその実装例である。

class AtoZ(object):
    """文字列"A", "B",...,"Z"と返す"""
    def __getitem__(self, index):
        if 0 <= index < 26: # 0から始まる!
            return chr(index + ord("A"))
        raise IndexError()

if __name__ == '__main__':
    for letter in AtoZ():
        print(letter, end="") # ABCDEFGHIJKLMNOPQRSTUVWXYZ
    else:
        print(end="\n")

__getitem__ メソッドは self[key] のような値評価を実現するために実装する。シーケンスの場合はkeyには整数値かスライスオブジェクトを受け付けるようにしなければならない。今回はシーケンスではなくマップ型と思えばこれでよい。
for letter in AtoZ(): とすると一体いつ AtoZ()[idx] と値評価を行っているのか一見わからないが、Pythonのfor文の仕様にそのヒントがある。引用すると

式リストは一度だけ評価され、これはイテラブルオブジェクトを与えなければなりません。 expression_list の結果に対するイテレータが生成されます。その後、イテレータが与えるそれぞれの要素に対して、インデクスの小さい順に一度づつ、スイートが実行されます。それぞれの要素は通常の代入規則でターゲットリストに代入され、その後スイートが実行されます。全ての要素を使い切ったとき (シーケンスが空であったり、イテレータが StopIteration 例外を送出したなら、即座に)、 else 節があればそれが実行され、ループは終了します。

http://docs.python.jp/3.3/reference/compound_stmts.html#for

つまり、

class AtoZ(object):
    """文字列"A", "B",...,"Z"と返す"""
    def __getitem__(self, index):
        if 0 <= index < 26: # 0から始まる!
            return chr(index + ord("A"))
        raise IndexError()

if __name__ == '__main__':
    for letter in iter(AtoZ()):
        print(letter, end="") # ABCDEFGHIJKLMNOPQRSTUVWXYZ
    else:
        print(end="\n")

という動きをしている(と私は解釈している)。

次に、Two-Argment form of built-in iter() function を見てみる。組み込み函数iter()のドキュメントを引用すると

イテレータオブジェクトを返します。 第二引数があるかどうかで、第一引数の解釈は非常に異なります。 第二引数がない場合、 object は反復プロトコル (__iter__() メソッド) か、シーケンスプロトコル (引数が 0 から開始する __getitem__() メソッド) をサポートする集合オブジェクトでなければなりません。これらのプロトコルが両方ともサポートされていない場合、 TypeError が送出されます。 第二引数 sentinel が与えられているなら、 object は呼び出し可能オブジェクトでなければなりません。この場合に生成されるイテレータは、 __next__() を呼ぶ毎に object を引数無しで呼び出します。返された値が sentinel と等しければ、 StopIteration が送出され、そうでなければ、戻り値がそのまま返されます。

http://docs.python.jp/3.3/library/functions.html#iter

とある。呼び出し可能オブジェクトとは特殊メソッド __call__ が実装されたものである。このドキュメントに従って実装すると以下のようになる。

class DDHs(object):
    __names = ("はるな", "ひえい", "しらね", "くらま",
               "ひゅうが", "いせ", "いずも", "24DDH")

    def __init__(self, first=None):
        self.index = (-1 if first is None else
                      DDHs.__names.index(first)-1)

    def __call__(self):
        self.index += 1
        if self.index < len(DDHs.__names):
            return DDHs.__names[self.index]
        raise StopIteration()

if __name__ == '__main__':

    for ddh in iter(DDHs("ひゅうが"), None):
        print(ddh, end=" * ") # ひゅうが * いせ * いずも * 24DDH * 
    else:
        print(end="\n")

    for ddh in iter(DDHs(), "いずも"):
        print(ddh, end=" * ") # はるな * ひえい * しらね * くらま * ひゅうが * いせ * 
    else:
        print(end="\n")
    try:
        for ddh in iter(DDHs(),): 
            print(ddh, end=" * ")
        else:
            print(end="\n")
    except TypeError as e:
        print("__iter__ か __getitem__ を実装しましょう")

1つ目の for ddh in iter(DDHs("ひゅうが"), None) の場合、sentinel=Noneであるので__namesが尽きるまで反復した後にStopIterationを投げる。*1
2つ目の for ddh in iter(DDHs(), "いずも") の場合、DDHs() から "いずも" が返された時にiter()がStopIterationを投げる。
3つ目の場合はiter()に引数を1つしか渡していないかつ DDHs が __iter__ か __getitem__ を実装していないためTypeErrorとなる。

最後はIterator protocol Iteratorsである。今まではiter()に丸投げしていたともいえるが、今度は自分で __iter__ と __next__ を実装するのである。実装例として標準ライブラリの collections.Counter風のものを作る。

class Bag(object):

    def __init__(self, items=None):
        self.__bag = {}
        if items is not None:
            for item in items:
                self.add(item)

    def __iter__(self):
        """ジェネレーターを利用している"""
        return ((item, count)
                for item, count in self.__bag.items())

if __name__ == '__main__':
    bag = Bag(list("And now for something completely different."))
    for c in bag:
        print(c, end="")
    else:
        print()

Iteratorのみに着目すればこのようになる。「今度は自分で __iter__ と __next__ を実装するのである」と言いつつ実際はジェネレーターに丸投げしているのが何ともいえない。
また、単純なものならばクラスを定義するよりもジェネレータを定義したほうが早いと思われる。

このように、どのようにイテレータが実装してあっても、ユーザーはただfor文を回せばよいことがわかる。よく

ls = ["A", "B", "C"]
for i in range(len(ls)):
    print(ls[i])

ではなく

ls = ["A", "B", "C"]
for c in ls:
    print(c)

と書くのがPythonicである、という話があるが、その理由がわかったような気がする。

*1: sentinelをこの業界風に訳すと番兵となるだろうか