2008年8月25日月曜日

Python のイテレータ (3)

以前、「Python のイテレータをジェネレータで作成」で、次のようなコードを書いた。

091126-005.png

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

    def __str__(self):
        return self.name + " " + str(self.age)
        
class Group:
    def __init__(self):
        self.persons = []

    def add(self, person):
        self.persons.append(person)
        return self
    
    # ジェネレータ。__iter__(), next() の置き換え。
    def iter(self):
        for person in self.persons:
            yield person
        

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

# iter() を呼出す
for person in group.iter():
    print person.name

for a in group.iter():
    print a.age

この書き方だと、イテレータを呼びだすときに、「Python のイテレータ」で __iter__(), next() を自前で書き、それに対して for ループを適用するときに比べて group.iter() と for ループで呼出さないといけないから、何だかなぁ~って思っていた。 (@_@;)

 

__iter__() に変更

ところで、イテレータとはそもそも何かと言えば、次のようにイテレータプロトコルに従っていればよい。

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

__iter__()

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

next()

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

(2.3.5 イテレータ型 より)

このことを考えると、上記のコードの場合、「Python のイテレータ」で __iter__(), next() を自前で書いていたときは Group クラスがイテレータプロトコルに従っており、Group クラス自体がイテレータオブジェクトということになる。しかし、上記で示したコードになった場合、Group#iter() の呼出しによりジェネレータが作成され、これがイテレータの役割をする。

ところで、ジェネレータは Python のジェネレータ (1) の最初の例で述べたように、

(…) yield を使って定義した関数が、あたかもオブジェクトのように振る舞っているように見える。さながら、 generator1() によってインスタンス化されたオブジェクトがあり、それが next() というメソッドを持っていて、呼出す度に yield で動作を止め、そのとき yield に渡された値を返し、そして最後の yield 呼出しが終ると次回 next() の呼出しで、StopIteration を投げる。

というようにイテレータプロトコルに従っている。つまり、イテレータオブジェクトと言えるのだろう。多分。 ^^;

話を戻して、上記に示したコードのようにジェネレータ関数を Group クラスに定義したということは、もはや Group クラスはイテレータプロトコルに従っているわけではなく、イテレータオブジェクトではなくなってしまっている。あくまでもイテレータオブジェクトなのは、Group 関数に定義したジェネレータ。当然ながら、Group クラスのインスタンスをそのまま for ループの中では適用できなくなっている。

これでは何かメリットが失われてしまったと感じるので、次のようにコードを修正した。修正箇所は簡単で、

def iter(self):

def __iter__(self): 

に変更するだけ。

091126-006.png

 

念のためコードを示す。

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

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

class Group:
    def __init__(self):
        self.persons = []

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

    # ジェネレータ
    def __iter__(self):
        for person in self.persons:
            yield person


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

これにより、for ループでの記述が以前のようにシンプルになった。

 

__iter__() の実装を変更

以前に、Beautiful Soup を使ったことがある。このソースコードの中では __iter__() が使われているだろうかと思って調べてみたら、 Tag クラスに定義されており、その実装は iter() 関数を呼出し、その引数に Tag クラスのインスタンス変数であるシーケンス型のオブジェクトが渡され、それが返されようになっていた。

return iter(self.contents)

2.1 組み込み関数 によると、

iter(o[, sentinel])

イテレータオブジェクトを返します。2 つ目の引数があるかどうかで、最初の引数の解釈は非常に異なります。2 つ目の引数がない場合、 o反復プロトコル (__iter__() メソッド) か、シーケンス型プロトコル (引数が 0 から開始する __getitem__() メソッド) をサポートする集合オブジェクトでなければなりません。…

(太字は引用者による)

これを真似するなら、上記の __iter__() の実装を変更する。

    def __iter__(self):
        return iter(self.persons)

こちらも一応全てのソースコードを示すと、

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

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

class Group:
    def __init__(self):
        self.persons = []

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

    def __iter__(self):
        return iter(self.persons)


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

うーん、どちらがいいんだろう…。 (@_@;)

追記 (2009.11.26) : 変更可能なシーケンス型 は、シーケンス型プロトコルに多分従っているだろうから、iter 関数使った方が効率的。反復プロトコル、または、シーケンス型プロトコルに従っていないタイプで要素を保持する場合、yield を使ってジェネレータを生成するのが良いだろう。

 

関連記事