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

タイトルは適当です。そしてパーマリンクから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 "", line 1, in
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 "", line 1, in
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 # ジェネレータ!

>>> 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 "", line 1, in
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 "", line 1, in
  File "", line 1, in test_send
IndexError
>>> next(g)
Traceback (most recent call last):
  File "", line 1, in
StopIteration

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

用途としては ジェネレータ を利用不可能にするとか、中の(多重)ループを抜けるのに例外で制御するとかかな。

申し訳ないが想像力がないのであんまり思い浮かばない。

関係を図でまとめると以下のような感じです。今回出てきてないmappingとかもあるけどスルーしてOKです。

iterable2

あくまで僕の頭の中のイメージなので、間違ってても責任は取りません(^▽^)

image1

image2

番外編

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

マジックメソッド

イテラブル , イテレータ といった特徴はオブジェクトに対して特定のマジックメソッドが存在するかどうかによって識別することができます。

イテラブル なオブジェクトは __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 "", line 1, in
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 "", line 1, in
AttributeError: 'listiterator' object has no attribute '__next__'
>>> i.next

>>> i.next()   # 検証に使ってる環境が2系なので.next()
1
>>> next(i)
2

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

中身を見たい

イテレータジェネレータ は状態を持つオブジェクトなので、そのままでは全容が見えません。

これはちょっとした小技ですが、listやtuple関数によって コンテナ に直して表示してあげましょう。

>>> i = iter([1, 2, 3])
>>> # 中身がわからない><
>>> i

>>> 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によって代入するオブジェクトを返却します。

値を返却した後にも処理が書けるという ジェネレータ の性質をうまく利用しているというわけです。

Warning

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 "", line 2, in
  File "/usr/lib/python2.7/contextlib.py", line 28, in __exit__
    raise RuntimeError("generator didn't stop")
RuntimeError: generator didn't stop

いかがだったでしょうか。

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

一緒に頑張っていきましょう。