(遅ればせながらアドベントカレンダーに登録しようと思ったら既にいっぱいだったので、通常記事として投稿)
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):
は等価であることが期待されます。
以下は、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例外を投げます。
擬似コードでジェネレータ風に書くと、こんな感じの動作となります。
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を発生させます。ジェネレータがもともと終了していた場合は、何もしません。擬似コードで書くと、こんな感じ?
def generator_close(gen):
try:
gen.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
return
throw RuntimeError
これもやはり使い道があまり思い浮かびません。必ず呼び出される保証もないわけですから、呼び出されることを前提としたものは書けないでしょう。
なお、特にそう明記してあるドキュメントは見当たらなかったのですが、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では、イテレータが多用されるが、その仕様について、分かっていたつもりになっていたこと、古い知識で止まっていたことも多かった。そのため、公式ドキュメントをベースに、言語仕様を復習してみました。
知らなかったことも結構ありましたが、その大半は、便利な使い道が思い浮かばないというのが正直なところです。
「他にもこんな仕様があるよ」とか「この機能はこう使うんだよ」ってのがあれば、コメント欄にどしどし書き込んでください。