1. ジェネレータを理解するためには、イテレータから
Python における リスト内包表記 を理解したので、次は、9.9 ジェネレータ (generator)。
ジェネレータは、イテレータを作成するための簡潔で強力なツールです。
と説明があるので、ジェネレータを理解する前に、
イテレータ
について確認する。
2. イテレータ の実装方法と使い方
9.8 イテレータ (iterator) によると、その役割は for 文と連携することにある。
for
文を使うとほとんどの コンテナオブジェクトにわたってループを行うことができます
Python のイテレータは、Java の For-each Loop に似ている。
- cf. Java: For-each Loop CCC
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 基本的なカスタマイズ によると、
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)
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)
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)
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])
0コメント:
コメントを投稿