🎾

【Python】 itertools.countでカウントダウンを実装する~イテレータ型とは~

に公開

今回の目標

$ python3 count_down.py
3
2
1
Hello, World!

itertools.count とは

ここからの説明は概要をつかむための説明です。ふわっとしています。
正しい説明はPythonの公式ドキュメントを参照してください。

まず、itertoolsモジュールとは何か

よく使われるイテレータを集めたPythonの標準ライブラリです。

ではイテレータ(iterator)とは何か

Pythonの型の一つです。イテレータ型[1]はざっくりいうと順番に取り出すことができるものです。
[1,2,3]みたいにfor文で一つずつ取り出せるものという認識でよいと思いますが、正確に言うと、list型はイテレータ型ではないようです。
ドキュメントによれば、イテレータ型とは__iter__関数と__next__関数を持っているオブジェクトという説明が正しいでしょうか。
list型は__next__関数を持っていませんので、イテレータ型とは言えないわけです。

>>> a = [1, 3]
>>> a.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__next__'. Did you mean: '__ne__'?

一方で、list型は__iter__を持っています。

>>> a = [1, 3]
>>> a.__iter__()
<list_iterator object at 0x7a972fa85450>

このように__next__は持っていないけど、__iter__を持っているオブジェクトをiterableというみたいです。[2]

余談ですが、list、tuple、dict、setなどはコンテナオブジェクトというらしく、このコンテナオブジェクトが__iter__を持っているとイテレータに変換可能みたいです。

__iter__は何?

イテレータを返す関数[3]です。
繰り返し処理を行うにはイテレータでないといけません。つまり、listなどはforなどを使うときに内部で一度イテレータにしているわけです。[4]
listやdict、tupleなどでは__iter__を使うことでイテレータが返ってきて、イテレータで__iter__を発動させると自分自身が返ってきます。
listなどはiter([1,3])のようにiter関数に渡すと__iter__と同じようにイテレータを作ることができます。

本当に自分自身なのか?

>>> a = [1, 3]
>>> b = a.__iter__()
>>> id(b)
134789771019744
>>> id(b.__iter__())
134789771019744

idが同じなので、イテレータとそのイテレータで__iter__()を使ったときに返されるイテレータは同じオブジェクトのようです。

じゃあ、__next__ってなんだ?

イテレータの次の要素を出力するものです。

>>> a = [1, 3]
>>> c = iter(a)
>>> c.__next__()
1
>>> c.__next__()
3
>>> c.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> c.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

順番に取り出していって、これ以上取り出すものがない場合はStopIteration例外を出します。
一度StopIteration例外を出したイテレータはその後、__next__で呼び出そうとすると、StopIteration例外を出します。
また、最初の要素からにする方法はないと思われます。(もう一度イテレータを生成しなおせばリセットできるようです。)

__next__は次のようにnext関数を使っても同じ結果となります。

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

__next__は私の中で「カーソルを一つ移動させて、通った要素を返す」といったイメージを持っています。

以上を踏まえてitertools.countとは?

>>> import itertools
>>> count_iterator = itertools.count()
>>> next(count_iterator)
0
>>> next(count_iterator)
1
>>> next(count_iterator)
2
...

itertools.countは呼び出されるたびに一つ大きくなった数字を返してくれるイテレータです。
正確には開始の数字と増える数字を指定することができます。

itertools.count(start=0, step=1)

startが開始の数字でデフォルトが0、stepが間隔でデフォルトが1となっています。

>>> import itertools
>>> count_test = itertools.count(start=3, step=17)
>>> next(count_test)
3
>>> next(count_test)
20
>>> next(count_test)
37

また、itertools.countは無限イテレータと分類されるイテレータを作成するものです。
これで作成されたイテレータは何度呼び出してもStopIteration例外は発生しません。

ちなみに、公式ドキュメントによると以下の実装とだいたい同じものらしいです。

def count(start=0, step=1):
    n = start
    while True:
        yield n
        n += step

ちょっと待った、yieldって何?

yieldを持っている関数は、ジェネレータと呼ばれています。
ジェネレータを呼び出すと、ジェネレータイテレータというオブジェクトを作成します。
ジェネレータイテレータがnext関数等で呼ばれると、yieldまで処理が走り、そこで中断され、yieldに渡された値を返します。(上記の場合はnが返る)そして、同じジェネレータイテレータがもう一度next関数等で呼ばれた場合、その中断された処理から再開します。

>>> def g():
...     print('one')
...     yield 1
...     print('two')
...     yield 2
... 
>>> ge = g()
>>> print(ge)
<generator object g at 0x7a972ef89c00>
>>> next(ge)
one
1
>>> next(ge)
two
2
>>> next(ge)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

公式ドキュメント[5]では循環した説明になっているため、非常にわかりにくいですが、おそらくイテレータを作れる関数をジェネレータと呼ぶという認識でいいと思います。

カウントダウンのプログラムを作る

3で始まり、-1ずつ増えていく(1ずつ減っていく)イテレータをfor文で回し、0になったらfor文を抜けるように作るといいですね。
このイテレータはcount_iterator = itertools.count(3, -1)と書けるので、

count_down.py
import itertools

count_iterator = itertools.count(3, -1)

for i in count_iterator:
    if i == 0:
        print('Hello, World!')
        break
    print(i)

と書くといいですね。
ちなみに、for文はStopIteration例外が出るまで繰り返すというのがちゃんとした説明みたいですね。
count_iteratorStopIteration例外を出さないので、breakで抜けないと一生for文から抜け出せなくなります。

まとめ

用語 説明
イテレータ(iterator, イテレータオブジェクト) 要素を順番に出力できるオブジェクト。__iter__メソッドと__next__メソッドを持つ。__next__で返す値がなくなった場合はStopIteration例外を出す。
iterable イテレータに変換できるコンテナデータ型のオブジェクト。__iter__メソッドを持つ。
コンテナデータ型(container datatype) オブジェクトを複数集めたオブジェクトの型。dictlistsettupleをPythonの組み込みコンテナと呼ぶ。
ジェネレータ ジェネレータイテレータを返す関数と公式で説明されているオブジェクト。要はyieldが途中で出てくる関数のこと。文脈によってはジェネレータイテレータを指しているときもある。この記事ではジェネレータはジェネレータ関数のことで統一してあります。itertoolsはジェネレータをまとめたモジュールだと思っている。ジェネレータが呼ばれると中はすぐに実行されず、ジェネレータイテレータが返る。
ジェネレータイテレータ 公式ではジェネレータ関数で作られたオブジェクトと説明されている。説明が循環している。イテレータの一種でyieldが組み込まれている関数から生成されたオブジェクトだと認識している。

今回はitertools.countの使い方と見せかけて、Pythonのイテレータ型の解説でした。
私も勉強し直せてよかったと思います。itertoolsはほかにも使えそうなものがたくさんあったので、記事にまとめたいと思いました。

おまけ

rangeで実装

count_down_range.py
for i in range(3, 0, -1):
    print(i)
print('Hello, World!')

まあ、これでなんの問題もないと思います。

参考

https://docs.python.org/ja/3.13/library/itertools.html#itertools.count

脚注
  1. 公式の説明: https://docs.python.org/ja/3.13/library/stdtypes.html#iterator-types ↩︎

  2. 公式の説明: https://docs.python.org/ja/3.13/glossary.html#term-iterable ↩︎

  3. 正確に言うと関数ではなくメソッド ↩︎

  4. forは内部で渡されたものをイテレータに変換してから使っているようです。なので、forに渡せるオブジェクトはiterableなもの、つまり、__iter__関数を持っているオブジェクトです。 ↩︎

  5. 公式の説明: https://docs.python.org/ja/3.13/glossary.html#term-generator ↩︎

Discussion