2008年6月3日火曜日

Python のイテレータ

1. ジェネレータを理解するためには、イテレータから

Python における リスト内包表記 を理解したので、次は、9.9 ジェネレータ (generator)

ジェネレータは、イテレータを作成するための簡潔で強力なツールです。

と説明があるので、ジェネレータを理解する前に、

イテレータ

について確認する。

 

2. イテレータ の実装方法と使い方

9.8 イテレータ (iterator) によると、その役割は for 文と連携することにある。

for 文を使うとほとんどの コンテナオブジェクトにわたってループを行うことができます

080329-004Python のイテレータは、Java の For-each Loop に似ている。

Ruby のイテレータ (2) - Enumerable で考えた同じ例を、Python のイテレータで実装してみる。例の内容は、

ex. 「人」が「グループ」 に所属している。「人」は `名前' と `年齢' を属性として持つ。

 

要素のとなるクラスの定義

まずは、 Person クラスから定義する。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name + " " + str(self.age)

__str__ は、print 文から呼出すために作成。

3.3.1 基本的なカスタマイズ によると、

__str__(self)

組み込み関数 str() および print 文によって呼び出され、オブジェクトを表す ``非公式の'' 文字列を計算します。

Python では、9.6 プライベート変数 に、

クラスプライベート (class-private) の識別子に関して限定的なサポートがなされています。__spam (先頭に二個以上の下線文字、末尾に高々一個の下線文字) という形式の識別子、テキスト上では _classname__spam へと置換されるようになりました。

とある。ここでは、上記の点について考慮しないことにする。

 

イテレータ となるクラスに __iter__(), next() メソッドを実装

次に、 Person の集合に対する責務を持つ Group クラスを定義する。ここで、__iter__ と next() を定義することによって、 for ループで使用可能となる。2.3.5 イテレータ型 によると、

イテレータオブジェクト自体は以下の 2 のメソッドをサポートする必要があります。これらのメソッドは 2 つ合わせて イテレータプロトコル を成します:

__iter__()

イテレータオブジェクト自体を返します。このメソッドはコンテナとイテレータの両方をfor および in 文で使えるようにするために必要です。...

next()

コンテナ内の次の要素を返します。もう要素が残っていない場合、例外 StopIteration を送出します。...

今回は、Group クラスのオブジェクト自体がイテレートオブジェクトになるようにした。イメージとして以下の通り。プロトコルをインターフェイスのように解釈。 (追記 2009.11.25)

091125-002.png

class Group:
    def __init__(self):
        self.persons = []
        self.index = 0      # next() が返す要素のインデックス

    def add(self, person):
        self.persons.append(person)
        return self

    def __iter__(self):
        u""" next() メソッドの定義されているイテレータオブジェクトを返す """
        return self

    def next(self):
        u""" コンテナ内の次の要素を返す

        呼出される度に次の要素を返す。
        次の要素がないときは、StopIteration 例外を投げる。
        """
        if self.index >= len(self.persons):
            self.index = 0
            raise StopIteration
        result = self.persons[self.index]
        self.index += 1
        return result

この実装だと、要素の最後までイテレートせずに途中でイテレートをやめてしまうと、次回のイテレートで途中からイテレートすることになる。イテレート用の別のクラスを作成した方がいいだろうか (?_?)

Person オブジェクトを追加するためのメソッド add では、メソッドチェーンできるように、自身を返すようにした。

ジェネレータで実装したものはこちら → Python のジェネレータ (1) - 動作を試す

 

イテレータ プロトコル を実装するクラスに for ループ を適用

では、このクラスを使って、 for ループを適用してみる。

group = Group().add(Person("Tarou", 21)).add(
                    Person("Hanako", 15)).add(
                    Person("Jiro", 15))

for person in group:
    print person.name

for a in group:
    print a.age

# __str__ が定義されているので、これで出力できる。
for person in group:
    print person

 

イテレータ プロトコル を実装するクラスを リスト内包記法 で使う

さて、Python ではイテレータを定義しても、Ruby の Enumerable モジュールをインクルードしたときのように、「いくつかのメソッドがもれなくプレゼント」ということはないようだ。 ^^; ただし、リスト内包表記を使えば、Ruby の map, select を使ったときのように、簡潔な表現ができる。

# 20歳より小さい年齢の人のリスト
print [x.name for x in group if x.age < 20]

# 'T' または 'k' を名前に含む人のリスト
import re
print [person.name for person in group if re.search('T|k', person.name)]

print [person.name for person in group
            if [char for char in person.name if char in ['T', 'k']]]

 

3. イテレータ プロトコル を実装するクラスを ソート

次に、Ruby のイテレータ (2) - Enumerable では、 Group クラスのオブジェクトに対して、 sort() を呼びだしている。これは、 Enumerable の sort() が、 Group クラスの each() を利用して実装している事による。 Python では残念ながら、そのような実装になっていない。

しかし、2.1 組み込み関数 に、イテレータが定義されたクラスをソートするための関数が定義されている。 ^^

sorted(iterable[, cmp[, key[, reverse]]])

ソートについては、以下のサイトを参考に。

 

要素クラスに比較メソッド __cmp__(self, other) を実装

上記の「2 クラスの比較」によると、ソート時におけるクラスのオブジェクトの比較は、

__cmp__ メソッドを使えば、比較方法を定義できます。

Person クラスに、以下のようにメソッドを追加した。これは Ruby で言う、 Comparable モジュールをインクルードして、 <=> を定義しているのに相当。

イメージとしては、以下のように、比較用のインターフェイスを実装した感じ。(追記 2009.11.25)

091125-003.png

    def __cmp__(self, other):
        result = cmp(self.age, other.age)
        if result != 0:
            return result
        else:
            return cmp(self.name, other.name)

cmp() については、2.1 組み込み関数 を参照。

追記 (2010.1.5) : if の検査で 0 は偽と見なされる条件式を使うなら、次のように書ける。

    def __cmp__(self, other):
        result = cmp(self.age, other.age)
        return result if result else cmp(self.name, other.name)

__cmp__ が定義してあると、例えば、次のように比較ができるようになる。

# Person オブジェクトの比較
# Person クラスに __cmp__(self, other) が定義されていること
p = Person('a', 25)
print  Person('b', 20) < p < Person('d', 30)

 

リスト内包記法 と sorted()

Group に対して、リスト内包記法を使ってソートした Person オブジェクトのリストを取得してみる。全体としては、以下のようなイメージ。(追記 2009.11.25)

091125-004.png

print [person.name for person in sorted(group)]

print [person.name for person in sorted(group,
            lambda x,y: cmp(y.age, x.age))]

前者は、Person クラスに定義された比較基準を元にして、後者は、比較基準を与える関数を渡してソートをした。

 

リスト内包記法 と reduce()

最後に、Ruby の inject に相当する reduce() を使って、全員の年齢の合計を求めてみる。

print reduce(lambda x,y: x+y, [person.age for person in group])

 

関連記事