以前、「Python のイテレータをジェネレータで作成」で、次のようなコードを書いた。
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):
に変更するだけ。
念のためコードを示す。
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 を使ってジェネレータを生成するのが良いだろう。