ここでは単純にカウントアップ(0から指定された範囲の整数値を順に返送)するイテレータを定義してみよう。これはスタート値やステップ値の指定がない、簡易的なrangeクラスのようなものと考えられる。インスタンス生成時に上限を指定すると(デフォルトは5とする)、上限に指定された回数だけ整数値を列挙した後はStopIteration例外を発生するようにしよう。
ここで定義するクラスはおおよそ次のようなものになる。
class CountUpIterator:
def __init__(self, limit=5):
# 上限とカウンターの初期化処理
pass
def __iter__(self):
# 自身を呼び出し側に戻す
pass
def __next__(self):
# 上限を超えるまではカウントアップして、その値を戻す
pass
このクラスのインスタンスが管理するのは、上限値と現在値だ。上限値はインスタンス生成時に__init__メソッドのパラメーターlimitに渡されるようにする(デフォルト引数値を「5」に指定)。現在値とはnext関数または__next__メソッドを呼び出した側に返送する値のことだ。上限値はインスタンス変数のself.limitで、現在値は同じくインスタンス変数のself.counterで管理するようにしよう。この上限値と現在値を__init__メソッドで初期設定すればよい。
また、__iter__メソッドは自分自身(self)を戻り値にするだけなので「return self」の1行で済むだろう。よって、これら2つのメソッドを定義すると次のようになる。
class CountUpIterator:
def __init__(self, limit=5):
self.limit = limit
self.counter = -1
def __iter__(self):
print('__iter__ method called')
return self
def __next__(self):
# 上限を超えるまではカウントアップして、その値を戻す
pass
__init__メソッドで「self.counter = -1」と初期値を「-1」にしているのは、__next__メソッドで値を返すときの処理を簡単にするためだ。また、__iter__メソッドには呼び出されたことを画面に表示するprint関数呼び出しも追加しておいた。
__next__メソッドでやることは、self.counterの値を1つ増やすこと、self.counterの値とself.limitの値を比較して、上限に達していればStopIteration例外を発生させること、そうでなければself.counterの値を返送することだ。これをコードにすると次のようになる。
class CountUpIterator:
def __init__(self, limit=5):
self.limit = limit
self.counter = -1
def __iter__(self):
print('__iter__ method called')
return self
def __next__(self):
print('__next__ method called')
self.counter += 1
if self.counter >= self.limit:
raise StopIteration()
return self.counter
__init__メソッドでself.counterの値を-1にしておいたので、その値を1つ増やすだけで、最初に返送する「0」という値にできる。__init__メソッドでself.counterの値を0にした場合にどんなコードを書けばよいかは自分で考えてみよう。
では、このコードでCountUpIteratorクラスを定義したら、その振る舞いを確認していこう。
まずは以下のコードを実行してみる。
countup_iter = CountUpIterator(3)
countup_iter2 = iter(countup_iter)
print(countup_iter is countup_iter2) # True
これを実行すると、次のようになる。
「__iter__ method called」と表示されていることから、iter関数を呼び出すと、__iter__メソッドが呼び出されることが確認できた。また、__iter__メソッドは自分自身を戻り値とするので、「iter(countup_iter)」の戻り値はcountup_iter自身だ。よって、その後の「countup_iter is countup_iter2」の値はTrueとなる。これは反復可能オブジェクトから複数のイテレータを取り出したときに、それらが別々のオブジェクトとなるのとは対照的だ*1。
*1 実際には反復可能オブジェクトの__iter__メソッドを呼び出したときにどういう処理が行われるかはコード次第なので、確実にそうなるとは筆者には断言できない。ただし、Pythonの用語集のiterator項では「コンテナオブジェクトは、自身を iter() 関数にオブジェクトに渡したり for ループ内で使うたびに、新たな未使用のイテレータを生成します」とあるので、少なくともPythonの組み込み型に関していえば、反復可能オブジェクトからイテレータを取り出すときには新規に作られたものが返されると考えてよいだろう(実際、反復可能オブジェクトからイテレータを取り出そうというときに、どこかで使用中のイテレータを返されるのではなく、新規にイテレータが作られる方が、多くの場合、使う側の意図に沿った振る舞いだといえるだろう)。
次に以下のコードを実行してみよう。上のコードでは「CountUpIter(3)」として、このクラスのインスタンスを生成している。つまり、上限値は「3」だ。そこで、以下のコードでは__next__メソッドを都合4回呼び出している。
print(next(countup_iter))
print(countup_iter2.__next__())
print(next(countup_iter2))
print(countup_iter.__next__())
これを実行すると、次のようになる。
まず、値を出力するたびに「__next__ method called」と表示されているので、next関数からも最終的には__next__メソッドが呼び出されているのが確認できた。次に、上限値を超えたら、StopIteration例外がきちんと発生していることも上の画面からは分かる。これらから、Pythonで一般にイテレータと呼ばれているものと同様な処理ができているといってよいだろう。
なお、このクラスを少し変更するだけで、ランダムな整数値(0〜100の範囲)を返送するイテレータも定義できる。参考までに以下にコードを示す。
from random import randint
class RandomIntIterator:
def __init__(self, limit=5):
self.limit = limit
self.counter = -1
def __iter__(self):
return self
def __next__(self):
self.counter += 1
if self.counter >= self.limit:
raise StopIteration()
return randint(0, 100)
このRandomIntIteratorクラスを定義して、今度は次のコードを試してみよう。
mylist = list(RandomIntIterator())
print(mylist)
これを実行すると次のようになる。
反復可能オブジェクトを必要とする多くの場所では、イテレータを置くこともできる。そのため、上のコードではlist関数に反復可能オブジェクトの代わりにRandomIntIteratorクラスのインスタンスを渡している。これにより5個のランダムな整数値を要素とするリストが定義されたというわけだ。
for文でも同様だ。for文では「in」に続けて反復可能オブジェクトを置くが、ここにイテレータを置いてもよい。
for num in RandomIntIterator(3):
print(num)
Pythonのドキュメント「for文」には「in」に続けて記述する式の「結果はイテラブルオブジェクト(反復可能オブジェクト)にならなければなりません」とある。また、その「結果に対するイテレータが生成されます」ともある。そして、反復可能オブジェクトから(iter関数と同様な仕組みを用いて)イテレータが取り出されるのであれば、イテレータ自身を戻り値とするイテレータもそこに置けるというわけだ。ただし、既に見たように反復可能オブジェクトからイテレータを取り出すときと、イテレータからイテレータを取り出すときではその動作が異なる。
イテレータをクラスレベルで定義しようとすると、今回見たようなコードになる。だが、「ジェネレータ」という仕組みを使うと、def文を使ってイテレータを戻り値とする関数を定義することも可能だ。これについては次回見ていくことにしよう。
今回は本連載で何度か登場していたイテレータについて、実際に定義をしながら、それがどんなものかを見た。次回は、イテレータをより簡便に定義可能なジェネレータについて見ていくことにしよう。
Copyright© Digital Advantage Corp. All Rights Reserved.