Pythonのイテレータ、ジェネレータまわりの言語仕様について復習してみる

  • 26
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

(遅ればせながらアドベントカレンダーに登録しようと思ったら既にいっぱいだったので、通常記事として投稿)
12/23追記: システムが変わって、アドベントカレンダーに登録したけど書けなかった人がいたら、乗っ取りができるようになったので、乗っ取らせていただきました。

はじめに

Pythonのイテレータ、ジェネレータまわりの言語仕様について、なんとなく知ってるつもりになっていたことや、いつの間にか付け加えられてたけど知らなかった機能が結構あったので、ここでちょっとまとめてみました。
(それらは正直いって、あまり使い道が見えてないので、詳しい方、コメントいただけるとありがたいです)
読者としては、イテレータやジェネレータをそれなりには知っている人を対象にしています。

なお、この記事では、特に断りなくitをイテレータを指す変数、Klassをユーザ定義のクラスとして扱います。xをオブジェクトを指す変数として扱います。

基本的なこと

Python2ではit.next(), Python3ではnext(it)

イテレータから次の要素を取り出す方法が、Python2と3とで変わりました。
Python2ではit.next(), Python3ではnext(it)です。
イテレータ風のクラスを自分で実装する場合、Python2ではKlass.nextを実装すればよく、Python3ではKlass.__next__を実装すればいいです。(この記事では、以下、Python3形式の書き方を採用します。)

イテレータを返すメソッド__iter__が実装されたクラスのオブジェクトをイテラブル(iterable)と呼びます

イテラブルなオブジェクトではiter(x)のように、イテレータを作ることができます。また、for文でinの後に指定したり、in演算子の右辺に指定することができます(in演算子は、__contains__があればそれを、なければ__iter__を試みます)。イテラブルの例には、list, tuple, dict, strやファイルオブジェクトなどがあります。イテレータそのものもイテラブルです。
または、__getitem__メソッドが実装されたクラスのオブジェクトもイテラブルです。__iter__が実装されておらず、__getitem__が実装されたクラスのオブジェクトから作られたイテレータiter(x)は、nextを呼ばれるたびにx[0], x[1], ... を返し、IndexErrorが投げられるとStopIteration例外を投げます。

イテレータが終了したらStopIteration例外

next(it)を続けていき、いずれ、次の要素がなくなったときは、StopIteration例外が投げられます。
Klass.__next__を実装する際は、これ以上返すものがない場合にはStopIteration例外を投げましょう。

イテレータとジェネレータは違います

「ジェネレータ」(generator)はイテレータ(iterator)を返す関数で、通常の関数と似ていますが、yield文を使います。ジェネレータ自身はイテレータではありません。また「ジェネレータ式」(generator expression)はイテレータを返す式で、リスト内包表記と似ていますが、角かっこのかわりに丸かっこで囲みます。

iter(it) is it

itをイテレータとしたとき、iter(it)は、itそのものを返すべきです。つまり、イテレータを実装する場合はKlass.__iter__(self): return selfなどとすべきです。
for文ではfor x in it:for x in iter(it):は等価であることが期待されます。
以下は、ititer(it)が異なる場合に起こることの一例です。

itとiter(it)
print(sys.version)  # ==> 3.4.1 (default, May 23 2014, 17:48:28) [GCC]

# iter(it)がitを返す場合
class Klass:
    def __init__(self):
        self.x = iter('abc')
    def __iter__(self):
        return self
    def __next__(self):
        return next(self.x)

it = Klass()
for x in it:
    print(x)  # ==> 'a', 'b', 'c'

# iter(it)がitを返さない場合
class Klass2(Klass):
    def __iter__(self):
        return iter('XYZ')

it = Klass2()
for x in it:
    print(x)  # ==> 'X', 'Y', 'Z'
print(next(it))  # ==> 'a'

こんなの、知ってました?

iter(callable, sentinel)形式のイテレータ

iterが、引数2つで呼び出された場合は、やはりイテレータを返すのですが、動作が非常に異なります。引数が2つの場合は、1つ目の引数は、イテラブルでなく、呼び出し可能オブジェクト(関数や、その他のcallメソッドを持ったオブジェクト)でなければいけません。これにより返されたイテレータは、nextを呼ぶたびにcallableを引数なしで呼び出します。返された結果がsentinelに等しい場合には、StopIteration例外を投げます。
擬似コードでジェネレータ風に書くと、こんな感じの動作となります。

2引数のiter
def iter_(callable, sentinel):
    while 1:
        a = callable()
        if a == sentinel:
            raise StopIteration
        else:
            yield a

Pythonの公式ドキュメントには、例えばファイルを空行が現れるまで読み込む場合に便利だと書かれています。

公式ドキュメントより引用
with open('mydata.txt') as fp:
    for line in iter(fp.readline, ''):
        process_line(line)

ジェネレータに値を送る generator.send

ジェネレータの中で、
v = (yield x)
のように書くと、ジェネレータが再開したとき、値をvにもらってくることができます。
通常のnextによってジェネレータが再開された場合は、vにはNoneが入ります。
nextの代わりにgen.send(a) のように、sendメソッドを呼び出すと、ジェネレータは再開され、vにはaが入ります。そして、nextを呼び出したときと同様に値がyieldされればそれを返し、何もyieldされなければStopIteration例外を投げます。
公式ドキュメントには、値変更機能のあるカウンターを例としてあげています。

公式ドキュメントより引用
def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

ちなみに。いきなりsendを使うことはできず、少なくとも1回はnextをしないと、sendは使えません。(TypeError: can't send non-None value to a just-started generatorとなります。) sendした値がどこへ行くかを考えると分かる通り、一度もnextされていない場合には、値の行き場所がないためでしょう。

ジェネレータに例外を送る generator.throw

generator.throw(type[, value[, traceback]])
で、ジェネレータが中断した位置に例外を発生させることができます。
ジェネレータが何か値をyieldしたらそれを返し、何もyieldされなければStopIteration例外を投げます。throwした例外は処理されなかったらそのまま伝播します。(正直、有効な使い道がさっぱり思い浮かびません)

ジェネレータを閉じる generator.close

ジェネレータが中断した位置でGeneratorExit例外を発生させます。GeneratorExit例外またはStopIteration例外が投げられた場合、generator.close()は、それで終わります。何か値が返ってきた場合はRuntimeErrorを発生させます。ジェネレータがもともと終了していた場合は、何もしません。擬似コードで書くと、こんな感じ?

generator.closeっぽい処理
def generator_close(gen):
    try:
        gen.throw(GeneratorExit)
    except (GeneratorExit, StopIteration):
        return
    throw RuntimeError

これもやはり使い道があまり思い浮かびません。必ず呼び出される保証もないわけですから、呼び出されることを前提としたものは書けないでしょう。
なお、特にそう明記してあるドキュメントは見当たらなかったのですが、for文でbreakをすると、ジェネレータに対しGeneratorExit例外が投げられるようです。

for文でbreakするとGeneratorExit例外が発生する
def gen():
    for i in range(10):
        try:
            yield i
        except GeneratorExit:
            print("Generator closed.")
            raise

for i in gen():
    break  # ==> "Generator closed." is printed.

[3.3以降]サブジェネレータへの委譲構文

ジェネレータ内で yield from expr (exprはイテラブルを返す式) と書くことで、exprを順次返すことができます。
sendを考えなければ、以下の2つのコードは等価です。

サブジェネレータへの委譲
def gen1():
    yield from range(10)
    yield from range(20)

def gen2():
    for i in range(10):
        yield i
    for i in range(20):
        yield i

sendがある場合、sendした値はサブジェネレータに渡されます。

まとめ

Pythonでは、イテレータが多用されるが、その仕様について、分かっていたつもりになっていたこと、古い知識で止まっていたことも多かった。そのため、公式ドキュメントをベースに、言語仕様を復習してみました。
知らなかったことも結構ありましたが、その大半は、便利な使い道が思い浮かばないというのが正直なところです。
「他にもこんな仕様があるよ」とか「この機能はこう使うんだよ」ってのがあれば、コメント欄にどしどし書き込んでください。