Python のジェネレータ (1) - 動作を試す のつづき
1. ジェネレータをどこで使うのか?
ジェネレータの動作について、何となく雰囲気を理解できた。しかし、どのような目的に使うのか、今一分からない
「あ!これはジェネレータを使うと、問題を解くのが楽だ。」
「ジェネレータを使った方がシンプルに書ける」
ということが分かるようになりたい。
イテレータを置き換えるジェネレータ
最初に覚えたことは、
ジェネレータを使うと、イテレータを簡単に作成できる。
ということ。イテレータとは、複数のオブジェクトに対して、要素を一つづつ辿るための手段。よって、ジェネレータは、複数の要素を辿るために利用できる。
例えば、Group クラスが Person クラスに対して責務があるとする。イテレータを自前で実装するには、境界条件を考えながら、イテレータプロトコルに沿うようにする。
自前で実装したイテレータは、ジェネレータで置き換えることができる。
イテレータを自前で実装することと比べると、ジェネレータを使った方がシンプルに書ける。
2. ジェネレータを使い、リストを走査する例
ジェネレータを使うことができる例を挙げる。
には、「リストの各々の要素に対して、右隣の要素との差をリストとして返す」処理の例が書かれている。
>>> a = [0,1,2,3,4,5] (略)… 一つ前の要素との差を求めたかった。a[1]とa[0]の差は1といった感じで、a=[0,1,1,1,1,1]になって欲しい。0番目は何もしない。
ジェネレータを使わずに定義するなら、
def sa(ary): result = [] for x in zip(ary[:-1], ary[1:]): result.append(x[1] - x[0]) return result def sa_wrapper(ary): return ary[:1] + sa(ary) print sa_wrapper([0,1,2,3,4,5])
関数 sa をリスト内包表記で置き換えると、
def sa(ary): return [b-a for a, b in zip(ary[:-1], ary[1:])]
この問題に対して、ジェネレータを使ってみる。
- 対象のリストの要素に対して、0 番目と 1 番目の要素から走査するジェネレータを2つ想定する。
- 各々のジェネレータから要素を順に取り出し、関数 zip でジェネレータ g1 から取り出した値を g2 から引いた値をリストにする。
- 最後に、元の先頭の要素を求めるリストの先頭にくっつける。
ジェネレータを使わない場合、
ary[:-1], ary[1:]
のようにして部分リストを取得する。この処理をジェネレータで置き換える。
def gen(ary, i): u""" リストの i 番目の要素から走査するジェネレータ """ for e in range(i, len(ary)): yield ary[e] a = [0,1,2,3,4] print [a[0]] + [a-b for a,b in zip(gen(a,1), gen(a,0))]
ジェネレータを使うメリット
ジェネレータは、イテレータのように要素を1つずつ取り出しては処理を行う。そのため、ジェネレータを使うメリットは、
ary[:-1], ary[1:]
のように部分リストを完全に取得する必要がないこと。もし、対象のリストがとても長く、
「要素を検査した結果、すぐに値を返す」
というような処理が含まれる場合、効率的に計算が行われる。
3. 再帰的な処理で置き換える
以下、ジェネレータとは関係がない。上記の例を再帰的な処理で置き換えてみる。
- 空のリストが来たとすると、そのまま返す。
- 要素が一つの場合も、何もせずにそのまま返す。
- 要素が二つのときは、2 番目の要素から先頭の要素を引いた値をリストにして返す。
- 要素が三つ以上のときは、同様に2 番目の要素から先頭の要素を引いた値をリストにしたものと、2 番目以降の要素を再帰的に呼出した結果のリストと結合して返す。
def rec(ary): if len(ary) <= 1: return ary elif len(ary) == 2: return [ary[1] - ary[0]] else: return [ary[1] - ary[0]] + rec(ary[1:]) def rec_wrapper(ary): if ary == [] or len(ary) == 1: return ary return [ary[0]] + rec(ary) a = [0,1,2,3,4] print rec_wrapper(a)
先頭の要素を、リストの先頭に持ってくる処理も再帰呼出しの中に含めるように変更する。
def rec2(ary): if len(ary) <= 1: return ary elif len(ary) == 2: return [ary[0]] + [ary[1] - ary[0]] else: return rec2(ary[:-1]) + [ary[-1] - ary[-2]] print rec2(a)
要素を指定できるようにする
上記では、ジェネレータを使って書いたときの柔軟さが失われている。
ジェネレータを使った書き方は、引数 i によって隣接する要素だけではなくて、i 個離れた隣の要素から引いた値のリストを返すことができる。上記の再帰関数も、同じように指定された i だけ隣の要素から引いたリストを返すように変更してみよう。
まずは、具体的な例で考える。例えば、i = 3 で、3 つ隣の要素から各要素を引いた値のリストを得る場合。このとき、リストが返されるために、元になるリストは少なくとも 要素を 4 つ持っていないといけない。 要素が 3 つ以下であれば、何せずに元のリストをそのまま返すとする。逆に 5 つ以上であれば関数を再帰的に適用する。
これを一般的に書くならば、
- 引数 i + 1 と、リストの大きさが同じ場合、 i 番目の要素から先頭の要素を引いた値をリストにして返す。
- 引数 i >= リストの大きさであれば、何せずにそのまま返す。
- それ以外のときは、再帰的に関数を適用
def rec3(ary,i): if len(ary) <= i: return ary elif len(ary) == i+1: return [ary[i] - ary[0]] else: return [ary[i] - ary[0]] + rec3(ary[1:],i) def rec_wrapper3(ary,i): if len(ary) <= i: return ary return [ary[0]] + rec3(ary,i) print rec_wrapper3(a,1)
同じように関数 rec2 も変更してみる。
def rec4(ary,i): if len(ary) <= i: return ary elif len(ary) == i+1: return [ary[0]] + [ary[i] - ary[0]] else: return rec4(ary[:-1],i) + [ary[-1] - ary[-1-i]] print rec4(a,1)
シンプルに見えるけど、見直してもすぐに理解できなくなってしまった。 パタッ(o_ _)o~†
処理を分割する
上記の定義は複雑すぎる。理由は、処理が適切に分割されていないため。
行なっている処理は、2つに分かれている。
- リストの各々の要素に対して、右隣の要素との差をリストとして返す。
- 対象の先頭要素を、結果の先頭に追加する。
予め Haskell で書いてみる。
sa (x:[]) = [] sa (x:y:xs) = y-x : sa (y:xs) sa_wrapper xs | length xs <= 1 = xs | otherwise = head xs : sa xs
Python で書きなおすと、
def sa(ary): if len(ary) <= 1: return [] else: return [ary[1]-ary[0]] + sa(ary[1:]) def sa_wrapper(ary): if len(ary) <= 1: return ary else: return ary[:1] + sa(ary)
これで読みやすくなった。
Python のジェネレータ (3) につづく…
0コメント:
コメントを投稿