銀月の符号

Python 使い見習いの日記・雑記

カウンタ変数? いやイテレータ

1週間以上ブログ更新しないのはけっこうあることだけど、1週間 Python コード書かなかったのはひさしぶり。

ネタはないけれど、無理やり作って書いてみるか。昔の自分に今の自分が Python 教えるならば…といった設定にしてみる。

ジェネレータとか enumerate とか

>>> i = 0
>>> for c in 'abc':
...   print i, c
...   i += 1
...
0 a
1 b
2 c

は、 1 ずつ増えるという処理と出力するという処理を分けることで、こうなる。

>>> def x(s):
...   i = 0
...   for c in s:
...     yield i, c
...     i += 1
...
>>> for i, c in x('abc'):
...   print i, c
...
0 a
1 b
2 c

「ループの中に本処理とデータの前処理がごっちゃになる」のをさけるため「ループ対象となるデータのほうを求める形に事前調整する」ようにする。ジェネレータを使うことでこういうのが書きやすいのが Python の魅力かな、と個人的には思っている。(これ、 Python じゃなくてもできるし、とか、言語に関係なくこういう分離ができないとプログラマとしてまずくないか、とか、この程度なら分けなくてよくね? というツッコミはありえる…)。

ちなみに x とほぼ同じ動作をする組み込み関数 enumerate がすでにあるので、これで置き換えて

>>> for i, c in enumerate('abc'):
...   print i, c
...
0 a
1 b
2 c

と書くのが普通の Python コード。

next とか itertools.count とか

同じ動作をするコードはこんな感じでも作れなくはない。 next でイテレータから一つ値を取り出せる。

>>> import itertools
>>> counter = itertools.count()
>>> for c in 'abc':
...   i = next(counter)
...   print i, c
...
0 a
1 b
2 c

手続きをならべる書き方と、欲しいものを定義する書き方の折衷案のような。

next と iter と StopIteration

for 文の裏でなにがおこなわれるのか探るため、さらにバラしてみる。

>>> chars = iter('abc')
>>> counter = itertools.count()
>>> print next(counter), next(chars)
0 a
>>> print next(counter), next(chars)
1 b
>>> print next(counter), next(chars)
2 c
>>> print next(counter), next(chars)
3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

iter で iterable オブジェクトからイテレータを得ることができる。 next にて、もう取り出せるものがなくなったら StopIteration 例外が送出される。 for 文は無限 while ループと next, StopIteration 例外を捕まえる try except で模倣できる。

print 文、print 関数の差異

これ書いていて思いついた別件。 Python 2 の print 文と Python 3 の print 関数との違う点を発見。

>>> chars = iter('abc')
>>> counter = itertools.count()
>>> try:
...   while True:
...     print(next(counter), next(chars))
... except StopIteration:
...   pass
...
0 a
1 b
2 c

3 が表示されない。「一つ目の引数? の出力」と「二つ目の引数? 評価時の例外送出」、どちらが先かが異なるということ。

print 文、 関数ともに、今回の例の場合、一つ目のイテレータから要素が一つ多く取り出されているのはたしか。こうしたコードで例外が送出されて状況復帰しようとした場合、各イテレータから取り出された要素の数が不ぞろいになるのはありえることなので注意がいる。そして、状況不明となりがちなので使用していたイテレータはまず再利用できないと思われ。なお、 itertools.izip でも同じ問題が起こる。