2008年7月13日日曜日

Python のジェネレータ (1) - 動作を試す

1. イテレータとはコンテナの要素を走査するためのオブジェクト

これまでに Python の リスト内包表記イテレータ について調べた。 次は、「ジェネーレータ」。

Python のジェネレータを理解するには、イテレータの理解が不可欠。ジェネレータについて調べる前に、イテレータの復習から行う。

 

a. イテレータの役割

Python のドキュメント「9. クラス」 における「9.8 イテレータ」の説明を確認すると、

イテレータの使用は Python 全体に普及していて、統一性をもたらしています。背後では、forコンテナオブジェクトiter() を呼び出しています。…

イテレータは、Python において特別扱いされており、for 文と密接な関わりを持っている。

… この関数は next() メソッドの定義されたイテレータオブジェクトを返します。 next() メソッドは一度コンテナ内の要素に一度に一つづつアクセスします。コンテナ内にアクセスすべき要素がなくなると、next()StopIteration 例外を送出し、for ループを終了させます。

(太字は引用者による)

まとめると、

  1. コンテナの役割を持つオブジェクトを for 文で利用すると、
  2. コンテナオブジェクトの iter() メソッドが呼びだされ、
  3. イテレータオブジェクトが返される。
  4. イテレータオブジェクトは、コンテナが持つ要素を一つずつアクセスするための操作 next() が定義されている。
  5. イテレータオブジェクトが要素を最後まで辿りきった後、next() が呼び出されると、StopIteration の例外が投げられる。

 

b. イテレータの例と、動作イメージ

イテレータを使った例を「Python のイテレータ 」で試した。この例では、

  1. Person クラスと、そのコンテナである Group クラスを想定し、
  2. Group クラスでイテレータプロトコルを実装。
  3. Group クラスに __iter__() と next() メソッドを実装し、
  4. Group オブジェクトに対して、for ループを適用。

動作イメージを、先ほどのドキュメントと照らし合わせると考えると、下図のようになる。

080711-001

この例では、コンテナである Group クラスが、イテレータの役割も担うように実装した。Group クラスとは別のクラスをイテレータオブジェクトとして生成していない。

Group クラスのオブジェクトに対して、for 文が適用されると、

  1. 組込み関数 iter() に「コンテナである Group オブジェクト」が渡されて、
  2. イテレータの役割をするものが返され 、
  3. next() の呼出しに応じて、コンテナの中身である Person オブジェクトが引き渡される。

 

c. イテレータ型は、後から追加された特別な機能

イテレータ型は、 Python に最初から備わっていた機能ではなく、

バージョン 2.2 で 新たに追加 された仕様です。(2.3.5 イテレータ型 より)

今使っている Python のバージョンは 2.5.2 。 バージョンを見ると、イテレータが追加されたのは、ちょっと前のようだ。Releases を見ると、2002 年くらいに追加された仕様であることが分かる。

 

d. イテレータを使う組み込みの関数

イテレータ型は、Python の組み込み型の中で重要な位置をしめている。

例えば、イテレータを使う関数に、2.1 組み込み関数list() がある。list 関数の引数は、

「シーケンス、反復処理をサポートするコンテナ、あるいはイテレータオブジェクトです。」

と書かれている。enumerate, filter, tuple もイテレータを利用する。

その点からして、イテレータは「Python 全体に普及していて、統一性をもたらしています」ということ。

複数の要素を持つオブジェクトを、操作する利便性のために存在するイテレータ。その代わり、利便性と引き換えに、イテレータを書くための特別なルールを把握しておく必要がある。

 

2. ジェネレータとはイテレータオブジェクトをお手軽に作成する手段

a. ジェネレータの存在意義

なぜ、最初にイテレータについて理解しておいたのか?

理由は、ジェネレータには、イテレータを生成するための手段に過ぎないから。

9. クラス の 9.9 ジェネレータ によると、

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

ジェネレータでは、特別な構文である yield を使用する。

ジェネレータは通常の関数のように書かれますが、何らかのデータを返すときには yield 文を使います。 …

突然、「関数のように」と言われても、何のことやら... (@_@;)

とりあえず、重要な点を読み進めておく。

… next() が呼び出されるたびに、ジェネレータは以前に中断した処理を再開します (ジェネレータは、全てのデータ値と最後にどの文が実行されたかを記憶しています)。

 

b. 基本的な動作を確認する

まずは、ジェネレータがどのように動作するのか確認する。

にある、サンプルが理解しやすい。

>>> def generator1():
... yield "first"
... yield "second"
... yield "third"
...
>>> gen = generator1()
>>> gen
>>> gen.next()
'first'
>>> gen.next()
'second'
>>> gen.next()
'third'
>>> gen.next()
Traceback (most recent call last):
File "", line 1, in ?
StopIteration
>>>

(上記サイトより、装飾は引用者による)

最初に注目したのは、関数において return ではなく yield と書かれていること。

yield と言えば、Ruby の yield を連想する。Ruby では、ブロック付きメソッドが呼出されたときに、ブロックに制御を渡すためのものだった。

Python の yield は、Ruby の yield とは機能が違う。

ちなみに、JavaScript は、ジェネレータを Python から仕入れている。

 

c. まるでイテレータオブジェクトが存在するようだ

上記のサンプルに戻り、コードを眺めていると、yield を使って定義した関数が、

あたかもオブジェクトのように振る舞っている

ように見える。

  1. generator1() の呼び出しによって、インスタンス化されたオブジェクトが存在し、
  2. そのオブジェクトが next() メソッドを持ち、
  3. next() を呼出す度に、yield で動作を止め、
  4. そのとき yield に与えた値を返す。
  5. 最後の yield 呼出しが終ると、次回 next() の呼出しで、StopIteration を投げる。

next() による呼出しと、StopIteration を投げる機能は、イテレータの仕様と同じ。

080711-002動作をイメージすると、

  1. yield を関数内で使うことによって、
  2. 関数の背後に関数を監視する誰かが存在するようになり、
  3. 関数との交渉は、すべてその誰かを通してやりとりをする

といった感じ。

この辺り、特別なルールと仕組みが存在するところは、個人的に好みではない。。

 

3. ジェネレータの型を調べてみる

では、これらの型がどのようなものか、組み込み関数である type() を使って調べてみる。

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

type(object)

object の型を返します。返される値は型オブジェクトです。

以下のコードを検査してみたら、

def func():
    return "func()"

def gen():
    yield "gen()"

print type(func)
print type(gen)

print type(func())
print type(gen())

結果は、次のようになった。

<type 'function'>
<type 'function'>
<type 'str'>
<type 'generator'>

func(), ge() の両者とも関数だけれど、返り値の型が異なっている。

  1. 普通の関数は、返される文字列の型である str 。
  2. yield を使った方は generator

となっている。

 

4. yield 文によりジェネレータ関数となる

関数の中で yield を使うと、return により値を返す、普通の関数ではなくなってしまう。

6.8 yield 文 によると、

yield 文は、ジェネレータ関数 (generator function) を定義するときだけ使われ、かつジェネレータ関数の本体の中でだけ用いられます。関数定義中で yield 文を使うだけで、関数定義は通常の関数でなくジェネレータ関数になります。

ジェネレータ関数が呼び出されると、ジェネレータイテレータ (generator iterator)、一般的にはジェネレータ (generator) を返します。ジェネレータ関数の本体は、ジェネレータの next() が例外を発行するまで繰り返し呼び出して実行します。

(太字は引用者による)

yield がどのように実装されている、説明を読むと、

yield 文が実行されると、現在のジェネレータの状態は凍結 (freeze) され、expression_list の値が next() の呼び出し側に返されます。ここでの ``凍結'' は、ローカルな変数への束縛命令ポインタ (instruction pointer)、および内部実行スタック (internal evaluation stack) を含む、全てのローカルな状態が保存されることを意味します:

(太字は引用者による)

簡単にいえば、呼出しの途中で冷凍パックして、後でチンして解凍ということ。

関数の呼出しにおいて、処理の途中で動作を止め、そのときの中間状態の値を返すことができるようにした仕組み。それが、ジェネレータ。

 

5. ジェネレータの呼出しを試してみる

a. for ループで呼出し

ジェネレータは、イテレータを作成するためのツールとして存在するので、for ループで利用することができる。

def gen2():
    yield "a"
    yield "b"
    yield "c"

for x in gen2():
    print x

結果

a
b
c

for ループを使えば、呼出し側では next() を使わなくても、ジェネレータの凍結と再開をすることができる。 next() の呼出しは for ループに任せられるため。

ジェネレータの動作ついて順を追ってみる。

  1. まず、 yield を関数内に記述することにより gen2() の呼出しでジェネレータが生成される。
  2. for 文は内部で  iter() によりジェネレータイテレータを取得し next() を呼出す。
  3. next() を呼出すごとに個々の yield で値が返され処理が止まり、関数の実行が凍結される。

 

b. yield を for ループの中で使う

上記は見れば明らかなように、関数内で yield を実行する数だけ、関数外から next() を呼出すことができる。例えば、次にように yield が for ループによって複数回繰り返された場合、その回数だけ next() が呼出されることになる。

def gen3():
    for i in range(1,6):
        yield i

for i in gen3():
    print i

結果、

1
2
3
4
5

 

c. リスト内包表記を使って

これまでに書いたジェネレータを、リスト内包表記を使って記述するなら、

print [x for x in gen()]
print [x for x in gen2()]
print [x for x in gen3()]

結果は、

['gen()']
['a', 'b', 'c']
[1, 2, 3, 4, 5]

 

6. イテレータをジェネレータで作成する

「ジェネレータはイテレータを生成するためのツール」

ということに目を向ける。

Python のイテレータ」では、クラスをイテレータにするために、Group クラスに next(), __iter__() メソッドを実装した。これをジェネレータで代用してみる。

  1. Group クラスの __iter_(), next() の代わりに、ジェネレータ関数である iter() を作成し、
  2. インスタンス変数である Person オブジェクトを要素とする配列をイテレートする

ように変更してみた。

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

こうすれば、next() を実装する手間は減る。

追記(2008.8.25) : 上記を少し変更 → Python のイテレータ (3)

 

7. ジェネレータの独立性

ところで、作成されるジェネレータは、作成されたジェネレータごとに独立している。

上記の gen4() のコードの続きに、以下のように書くと、

g = gen4()
g2 = gen4()

try:
    print "g:" , g.next()
    print "g:" , g.next()
    
    print "g2:", g2.next()
    print "g2:", g2.next()
    
    print "g:" , g.next()
    print "g:" , g.next()
except StopIteration:
    print "StopIteration"
    
print "g2:", g2.next()

結果は、二つのジェネレータが各々の文脈を持っていることがわかる。

g: Tarou
g: Jirou
g2: Tarou
g2: Jirou
g: Hanako
g: StopIteration
g2: Hanako

 

8. ジェネレータ関数の中で return

ジェネレータというと、 最初に return を yield で置き換えるというイメージが強かった。そのため、 return を併用することができないかと思いきや、そうではない。 yield と return は同居できる。ただし、 return で値を返そうとするとシンタックスエラーとなるので注意。

例えば、 ジェネレータ関数の中で return “hoge” と記述すると、

exceptions.SyntaxError: 'return' with argument inside generator (line 80, offset 0): 'return "hoge"'

また、ジェネレータ関数内で return に到達すると、そこでおしまい。それ以降のコードは実行されないようだ。

例えば、次のジェネレータ関数において、 値 hoge3, hoge5 は返されない。

def gen5(n):
    if(n < 10):
        yield "hoge"
        yield "hoge2"
        return
        yield "hoge3"
    else:
        yield "hoge4"
        return
        yield "hoge5"

 

9. ジェネレータに値を渡すsend

7 PEP 342: New Generator Features によると、

In Python 2.3, yield was a statement; it didn't return any value. In 2.5, yield is now an expression, returning a value that can be assigned to a variable or otherwise operated on:

val = (yield i)

Python 2.5 で yield は式になったとある。

ちなみに、式とは Statement (programming) - Wikipedia, the free encyclopedia には、

In most languages statements contrast with expressions in that statements do not return results and are executed solely for their side effects, while expressions always return a result and often do not have side effects at all.

式とは、値を返すもの。 Haskell の let 式を思い出した。

let 式は式なので let 自体も値を持ちます。しかし where 節は節なので、値を持ちません。

(ふつうのHaskellプログラミング , p187)

Ruby の if も思い出した。

Rubyでは(Cなどとは異なり)制御構造は式であって、何らかの値を返すものがあります

(制御構造 - Rubyリファレンスマニュアル)

サンプルを試してみる。

def gen6():
    val = (yield "hoge")
    if val is not None:
        yield val + "fuga"
    else:
        yield "fuga"

g6 = gen6()
print g6.next()
print g6.send("piyo")
5.2.8 Yield expressions によると、

send(value)

Resumes the execution and ``sends'' a value into the generator function.

next() と同じように、ジェネレータを再開するときに send を用いることができる。 next() との違いは、呼出し側からジェネレータに値を渡すことができること。

上記のコードでは、

  1. 最初の next() の呼出しにより、yield “hoge” で一時停止し、
  2. 次の send(“piyo”) によって処理が再開されたときに、val に値が設定される。
  3. そして、もし send() ではなく、next() が呼出されたときは、val の値は None となってしまうのでそれに対応するために if で場合分けを行い対応している。

 

10. Ruby の yield と比較する

Ruby のイテレータ」 と、Python のジェネレータは類似している。

Ruby の例では、

  1. Person クラスは Group クラスに所属し、
  2. Group は Enumerable モジュールをインクルードすることによって、イテレータを実現。
  3. その際、Enumerable モジュールの機能を利用するために、 Group クラスで each メソッドを実装。
  4. このとき each メソッド内において、Group の要素である個々の Person を yield で引数としてブロックに渡し、制御を呼出し側に委ねる

Python のジェネレータでは、

  1. yield を関数の中で使うと、
  2. ジェネレータが生成される。

繰り返しになるが、ジェネレータは

「イテレータを簡単に作成するためのツール」。

Ruby は、クラスとインスタンス変数により、イテレータを実現している。この対比としてジェネレータを捉えると、Python では Ruby と同じような動作を、関数とローカル変数で簡便に行うことができる仕組みが備えられていると言える。ただし、同じようなと言っても、関数の呼出しにおいて相互に制御が移るという点だけだけれど。

Ruby のイテレータ で書いた例と、類似するコードを考えてみる。

def gen4():
    persons = ["Tarou", "Jirou", "Hanako"]
    for person in persons:
        yield person

for person in gen4():
    print person

print ["Hello! " + person for person in gen4()]

結果は、

Tarou
Jirou
Hanako
['Hello! Tarou', 'Hello! Jirou', 'Hello! Hanako']

Ruby で記述した Group クラスが ge4() 関数に相当し、Person クラスがローカル変数 persons に相当すると考えればいいかな。

 

内部イテレータなのか?

まつもと直伝 プログラミングのオキテ 第5回(2) - まつもと直伝 プログラミングのオキテ:ITpro によると、

Rubyのブロックのような個々の要素ごとの処理を表現するものをコンテナ・オブジェクトのメソッドに渡し,メソッドが要素ごとの処理を呼び返すタイプの繰り返し方法を「内部イテレータ」と呼びます。

イテレータ – Wikipedia によると、

内部イテレータとは、要素を格納したオブジェクトが、自身の要素に対して繰り返しながらユーザコードを逐一呼び出す方式である。繰り返しの記述が(オブジェクトの)内部にあるためこの名がある。一般にForeach文などとして言語で用意されている。

Ruby のイテレータは、処理をコンテナのメソッドに渡し、要素を保持しているオブジェクト側で繰り返しの記述があるので内部イテレータ。Python のジェネレータでは、繰り返しの記述はジェネレータ関数にある。しかし、呼出し側で for 文を使い、繰り返し next() で一時停止を解除している。これをどちらに分類すればいいのだろうか?

 

11. まとめ

追記 (2009.11.25):

  • 関数の中に yield があったら、そこで一時停止して値が返される。
  • 再開するには呼出側で next() を呼出す。
  • for は内部でジェネレータの next() を呼出してくれる。
  • 呼出し側からジェネレータに値を渡したい場合、send() を使う。

Python のジェネレータ (2) につづく…