[Python] 部屋とYシャツとイテレータとジェネレータと私

タイトルは適当です。そしてパーマリンクからroomが抜けました。

今回は初学者にとってわかりづらいイテレータ, ジェネレータなどの概念について簡単に解説しようと思います。

適切なデータ構造を選択するのはプログラマの必須スキルだとばっちゃが言ってたのでぜひマスターしておきましょう。
対象とするPythonのバージョンは2.7以上(もちろん3.0以上も含む)です。
想定する読者のレベルは初中級者です。ジェネレータのあたりは少し難しいかも。

なんで初学者にとっての記事なのに初中級者向けなんでしょう。私にも訳が分かりません。
ちょっとずつ読み進めてください..

本題に入る前に用語について簡単に解説していきます。

イテラブル(iterable)

イテレーション可能な構造をイテラブルといいます。
ではイテレーションとは何かと言われれば「繰り返すこと」「次要素にアクセスすること」と言えるでしょうか。
簡単に言うのであればfor文で回せるオブジェクト。厳密にいうのであればイテレータにできるオブジェクトかな。

今回の記事の中では一番大きな概念というか外側の集合を指します。

コンテナ(container)

要素をもつ入れ物オブジェクトをコンテナと表現することがよくあります。
正式にそういったものが定義されているわけではなく、そのように表現されることが多い、という話です。
基本的な型でいえばlist, tuple, dict, set, frozensetです。これらの拡張型であるcollectionsモジュールのオブジェクトなどもコンテナ型です。

上記で挙げたコンテナはいずれもイテレーション可能、つまりイテラブルなオブジェクトであると言えます。
逆にイテラブルじゃないコンテナは見たことがないので、関係は「イテラブルコンテナ」という理解でも今のところ間違いはないでしょう。

シーケンス(sequence)

シーケンス型はイテレーションしたときに要素の取り出し順が保証されているオブジェクトを指します。必ずしもコンテナ型ではありません。
文字列(Unicode)型、リスト、タプルといった一次元のオブジェクトがシーケンスです。これらはdictとは異なり、インデックス(数値)のみによって添え字を指定できます。

シーケンス型

イテレータ(iterator)

ではイテレータとはなんでしょうか。状態(イテレーションした場所)を記憶しているイテラブルなオブジェクトです。
イテレータイテラブルなオブジェクトの一つですが、イテラブルなオブジェクトからイテレータを作ることは可能です。それを実現するのがiter関数です。

>>> l = [1, 2, 3]
>>> i = iter(l)
>>> next(i)
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

イテレータはnextメソッドによって状態を進めることができ、イテレーションが不可能な状態になる(この場合だと最後の要素に達する)とStopIterationという例外を発生させます。
StopIterationに限らず、一度例外を送出したイテレータオブジェクトはもう使用できない点に注意してください。あとから参照先のオブジェクトに要素が追加されてもイテレータはアクセスできないということです。

イテレータについて知らないと、例えばリストを読み進めた場所を覚えておきたいときにindexという変数に添え字を記録してループの度にインクリメントする処理を追加しないといけなかったかもしれませんね。
特性を知っていればこの辺の仕事を楽にすることができますね。

今の話でお気づきかもしれませんが、実はイテレータを進めることができるのはnext関数だけではありません。(next関数はもっとも単純なアクセス方法です)

次の例ではfor文にイテレータを与えています。

リスト イテレータ
>>> l = [1, 2, 3]
>>> for _ in l: print(_)
...
1
2
3
>>> for _ in l: print(_)
...
1
2
3
>>> i = iter([1, 2, 3])
>>> for _ in i: print(_)
...
1
2
3
>>> for _ in i: print(_)
...
>>>

2つの違いがわかるでしょうか?
リストをfor文で回したときは毎回すべての要素にアクセスしますが、イテレータは最初のfor文でのみ要素にアクセスしていますね。
上記の例では一回目のfor文で最後の要素まで達してしまうわけですが、リストは状態を持たないのでfor文の度に先頭まで戻り再度全要素にアクセスしてしまいます。
これに対しイテレータは一度目のfor文で最後まで達したという状態を持つため2回目はループに入りません。(この例ではね)

当然ですがイテレータはシーケンスとは異なり、添え字によるランダムアクセスができず順次アクセスのみ可能です。

>>> [1, 2, 3][0]
1
>>> iter([1, 2, 3])[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'listiterator' object has no attribute '__getitem__'

さて、そろそろイテレータイテラブルがゲシュタルト崩壊してきたころではないでしょうか。
大丈夫、ここからはジェネレータに登場してもらいます。

ジェネレータ(generator)

大丈夫じゃない、問題だ。
いや、でも少しだけ安心してください。実はジェネレータはイテレータの仲間です。

ジェネレータは規則に則って値を作りだします。
規則を関数によって記述するのですが、普通の関数とは違いyield文を用います。
yield文を含めることにより、その関数はジェネレータを返すようになります。
「関数がジェネレータ」ではなく、関数によって返ってきたものがジェネレータです。ご注意ください。

値をインクリメントするジェネレータを作ってみましょう!

>>> def counter(start=0, step=1):
...     while True:
...         yield start
...         start += step
>>> g = counter(start=5, step=2)
>>> g # ジェネレータ!
<generator object counter at 0x6ffffe61a00>
>>> next(g)  # 5
5
>>> next(g)  # 5 + 2
7
>>> next(g)  # 5 + 2 + 2
9

returnの代わりにyieldによって返す値を返却するというわけです。yield文を実行する数がイテレーション可能な回数です。
ただし値を返却するのは呼び出しのタイミングではなくイテレーションしたタイミングです。
上記の例では「初期値(5)に増減値(2)を加算する」という規則をイテレーションのたびに適用して返却しています。

省メモリ

一般的にジェネレータとは上記のように都度値を生成するという性質を持つため、一般的に省メモリと言われます。個人的にはこれは側面的な特徴だという認識です(絶対ではないので
たとえば10000回ループする際に1インクリメントした値がほしいといった場合に、[0, 1, 2, …, 9999]といったリストをメモリに持つよりもループするタイミングで1加算していったほうがメモリにやさしいですよね。

また、先ほどの例のように無限ループをつくり、明確な終了条件を持たせなければ無限にイテレーションするオブジェクトを作成できるのはジェネレータの大きな特徴でありメリットです。
コンテナでループを表現しようとするとその分メモリを使うのでジェネレータはもってこいです。

ただし、省メモリだから必ずしも処理速度も速いというわけではないということにも注意してください。
返却値の生成(関数の実行)時間が速度要件に引っかかるのであれば、作成済のリストを渡すということも時には必要かもしれないということです。これは少し極端な例ですが

sendメソッド

実はyield文は左辺を持てます。sendメソッドの引数を左辺に渡すといった動作をします。
まぁ実際の動作を見るのが一番わかりやすいでしょう。先ほどのcounterで現在値を変更できるように改修してみます。

>>> def counter(start=0, step=1):
...     while True:
...         received = yield start
...         if received is None:
...             start += step
...         else:
...             start = received
...
>>> g = counter(start=5, step=2)
>>> next(g)
5
>>> next(g)
7
>>> next(g)
9
>>> g.send(0)
0
>>> next(g)
2

大体のイメージがつかめますか?
この例ではsendで受け取った値でstartをリセットしています。sendメソッドが実行されないときはNoneが入ります。
このようにジェネレータの状態変更用途に適したメソッドです。

注意点としては実行のタイミングです。sendはyieldよりも先に実行されます。そしてイテレーションも進みます。
上記のように例では「受け取った値がNoneの時には加算しない」といった制御をしなければ受け取った値がいきなり変更されてしまうことになります。

「sendによって値を受け取るyield」と「値を返却するyieldは別」と考えるのがわかりやすいです。

>>> def test_send():
...     r0 = yield 1
...     print(r0)
...     r1 = yield 2
...     print(r1)
...     r2 = yield 3
...     print(r2)
...
>>> g = test_send()
>>> # 始まってないジェネレータに対してsendできない(前のyieldがないからsendできない)
>>> g.send(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> next(g)
1
>>> g.send(0)
0
2
>>> g.send(0)
0
3

前のyield文で受け取ってからyieldで値を返却していますね。
このように書くと当たり前のように見えますが、ループで表現するとハマる確率アップです。

throw

外側から例外を発生させるのにthrowメソッドを使います。引数に例外オブジェクトを指定します。

>>> g = test_send()
>>> g.throw(IndexError)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in test_send
IndexError
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

イテレータと同様、一度例外を発生させたジェネレータは再利用不可です。

用途としてはジェネレータを利用不可能にするとか、中の(多重)ループを抜けるのに例外で制御するとかかな。
申し訳ないが想像力がないのであんまり思い浮かばない。

関係を図でまとめると以下のような感じです。今回出てきてないmappingとかもあるけどスルーしてOKです。
iterable2
あくまで僕の頭の中のイメージなので、間違ってても責任は取りません(*^▽^*)

番外編

ここからは脱線です。興味のある方だけご覧ください。

マジックメソッド

イテラブル」「イテレータ」といった特徴はオブジェクトに対して特定のマジックメソッドが存在するかどうかによって識別することができます。
イテラブルなオブジェクトは__iter__メソッドによってイテレータを返却する必要があり、もちろんfor文に入るときもコールされます。

>>> class TestIter(object):
...     def __iter__(self):
...         print('__iter__が呼び出されました')
...         return iter([1, 2, 3])
...
>>> for i in TestIter(): print(i)
...
__iter__が呼び出されました
1
2
3
>>> # イテレータじゃないとだめ
>>> TestIter.__iter__ = lambda self: [1, 2, 3]
>>> for i in TestIter(): print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: iter() returned non-iterator of type 'list'

for文は内部でイテレータを進めているだけということが理解できれば十分です。

ちなみにイテレータの場合は自分自身を返却するといった動作をします。
当然といえば当然ですね。だってイテレータが必要なだけなんだから。

>>> i = iter([1, 2, 3])
>>> i is i.__iter__()
True

またイテレータは「next」メソッド(2系の場合)、「__next__」メソッド(3系の場合)が存在することがイテレータの条件です。

>>> i = iter([1, 2, 3])
>>> i.__next__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'listiterator' object has no attribute '__next__'
>>> i.next
<method-wrapper 'next' of listiterator object at 0x6ffffe6bcd0>
>>> i.next()   # 検証に使ってる環境が2系なので.next()
1
>>> next(i)
2

実際にイテレーションを進める場合はインスタンスメソッドを使うよりも、バージョンの違いを吸収できるnext関数を使うべきです。

中身を見たい

イテレータジェネレータは状態を持つオブジェクトなので、そのままでは全容が見えません。
これはちょっとした小技ですが、listやtuple関数によってコンテナに直して表示してあげましょう。

>>> i = iter([1, 2, 3])
>>> # 中身がわからない><
>>> i
<listiterator object at 0x6ffffe6bd10>
>>> list(i)
[1, 2, 3]
>>> # 2回目は見られない><
>>> list(i)
[]

注意点としては、コンテナ化した際に全部の要素にアクセスしているためその後の利用は不可能という点です。

コンテキストマネージャ(contextmanager)

Pythonでは2.6以上のバージョンで「with」という構文が利用できます。(2.5ではfrom __future__ import with_statementすれば使える)
有名なのはopen関数で作成されたファイルオブジェクトなどでしょうか。
withコンテキストの区間に出入りするときの処理を「__enter__」「__exit__」メソッドに記述する必要がありますが、それはめんどくさいですね。

ここで使えるのがcontextlib.contextmanagerと呼ばれるユーティリティ関数です。
この関数はジェネレータを返す必要があります。

>>> from contextlib import contextmanager
>>> @contextmanager
... def test_context(something):
...     print('前処理')
...     yield something
...     print('後処理')
...
>>> with test_context('何らかの処理') as process:
...     print(process)
...
前処理
何らかの処理
後処理

yield文を挟んで前処理と後処理を記述、yield文ではasによって代入するオブジェクトを返却します。
値を返却した後にも処理が書けるというジェネレータの性質をうまく利用しているというわけです。

yieldの数は一つでないとエラーになりますので注意してください。

>>> from contextlib import contextmanager
>>> @contextmanager
... def test_context(something):
...     print('前処理')
...     yield something
...     yield something
...     print('後処理')
...
>>> with test_context('何らかの処理') as process:
...     print(process)
...
前処理
何らかの処理
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/usr/lib/python2.7/contextlib.py", line 28, in __exit__
    raise RuntimeError("generator didn't stop")
RuntimeError: generator didn't stop

いかがだったでしょうか。
ここに書いてあることはほんの一部ですが、様々なオブジェクトの特性を知っていれば効率的できれいなコードをかけるようになります。
一緒に頑張っていきましょう。