~~pythonのfor文の挙動について勘違いしているかもしれないので、理由に心当たりがありましたら教えていただけるとうれしいです。~~解決しました。
準備
同じ文字列を3回返すイテレータ
# coding: utf-8
class Iterator(object):
def __init__(self):
self.counter = 3
def __iter__(self):
return self
def next(self):
self.counter -= 1
if self.counter < 0:
raise StopIteration
return "わーい!"
確認
>>> from iter import Iterator
>>> instance = Iterator()
>>> iterator = iter(instance)
>>> print iterator.next()
わーい!
>>> print iterator.next()
わーい!
>>> print iterator.next()
わーい!
>>> print iterator.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "iter.py", line 13, in next
raise StopIteration
StopIteration
for文もいける
>>> for i in Iterator():
... print i
...
わーい!
わーい!
わーい!
本題
Iteratorを継承し、初期化時に元のイテレーションを1回だけ進めた後、nextメソッドを置き換えるRunTimeIterator
# coding: utf-8
class Iterator(object):
def __init__(self):
self.counter = 3
def __iter__(self):
return self
def next(self):
self.counter -= 1
if self.counter < 0:
raise StopIteration
return "わーい!"
class RunTimeIterator(Iterator):
def __init__(self):
super(RunTimeIterator, self).__init__()
for i in self:
print i
break
self.next = self.alter_next
def alter_next(self):
self.counter -= 1
if self.counter < 0:
raise StopIteration
return "たーのしー!"
確認
>>> from iter import RunTimeIterator
>>> instance = RunTimeIterator()
わーい!
>>> iterator = iter(instance)
>>> print iterator.next()
たーのしー!
>>> print iterator.next()
たーのしー!
>>> print iterator.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "iter.py", line 28, in alter_next
raise StopIteration
StopIteration
さてこれをfor文で動かすと、
>>> for i in RunTimeIterator():
... print i
...
わーい!
わーい!
わーい!
なぜかうまくいかない。
なんで?
わかんないや。
nextメソッドを直接置き換えようとすると起きる問題らしく、上記の例なら返す文字列をインスタンス変数に保持して置き換えれば普通に動くし、より一般的にはnextメソッドが別のメソッドの実装を返すようにし、そちらを置き換えるようにするとうまくいく。
実はnextメソッドを直接呼ばず、build-inのnext()関数を使うとfor文と同じ現象が起きる。
>>> from iter import RunTimeIterator
>>> instance = RunTimeIterator()
わーい!
>>> iterator = iter(instance)
>>> print next(iterator)
わーい!
>>> print next(iterator)
わーい!
>>> print next(iterator)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "iter.py", line 12, in next
raise StopIteration
StopIteration
for文の挙動としては、イテレータオブジェクトのnextメソッドを直接呼ばずに、next()関数を用いてイテレーションを行っているようだ。
そもそもnext()関数ができた理由が、__iter__や__len__を直接呼ばずiter()やlen()を使うように、nextメソッドを直接呼ばずに済むようにする(加えてpython3での__next__への改名の都合がある?)ためなようなので1、インスタンスのnextメソッドを直接呼ぶのはあまり推奨されないようだ。
結局の所、問題なのはbuild-inのnext()が返すのが、イテレーションを行う時点でのインスタンスのnextメソッドではなく、クラス定義時のnextメソッドになっている点なのだが、どうやらイテレータの実装はクラス定義時にセットされるのが原因らしい2。
インスタンスのnextメソッドを置き換えても実際のイテレータには反映されず、クラスのnextメソッドを直接書き換えると動くには動く。
class RunTimeIterator(Iterator):
def __init__(self):
super(RunTimeIterator, self).__init__()
for i in self:
print i
break
# self.next = self.alter_next
self.__class__.next = self.alter_next
def alter_next(self):
self.counter -= 1
if self.counter < 0:
raise StopIteration
return "たーのしー!"
とはいえクラスレベルで置き換えてしまうと、2つ以上インスタンスを作った時にはもちろんこうなる。
>>> from iter import RunTimeIterator
>>> for i in RunTimeIterator():
... print i
...
わーい!
たーのしー!
たーのしー!
>>> for i in RunTimeIterator():
... print i
...
たーのしー!
たーのしー!
たーのしー!
解法?
nextメソッドは直接置き換えられないので、nextメソッドが別のメソッドの実装を返すようにして、置き換えたい時にはそちらを換えるようにする。
次の例では、nextメソッドは_nextメソッドの結果を返すようにし、nextメソッドを置き換えたい時には_nextメソッドを置き換えている。
class RunTimeIterator(Iterator):
def __init__(self):
super(RunTimeIterator, self).__init__()
for i in self:
print i
break
self._next = self.alter_next
def next(self):
""" 本物の`next`メソッドだけど中身は`_next`
"""
return self._next()
def _next(self):
""" 実質的な`next`メソッド
初期状態として親の`next`メソッドを使うようにする。
`_next`メソッドを定義する代わりに、`__init__`に
self._next = super(RunTimeIterator, self).next
と書いても同じ。
"""
return super(RunTimeIterator, self).next()
def alter_next(self):
self.counter -= 1
if self.counter < 0:
raise StopIteration
return "たーのしー!"
>>> from iter import RunTimeIterator
>>> for i in RunTimeIterator():
... print i
...
わーい!
たーのしー!
たーのしー!
>>> for i in RunTimeIterator():
... print i
...
わーい!
たーのしー!
たーのしー!
元々、ある形式のファイルからヘッダーに当たる領域を何行かパースして、それ以降は行ごとに必要なデータだけ取って返すパーサーを、fileを継承したクラスで書きたくてnextメソッドを置き換えるなんてことを試していたらこの問題に遭遇してしまった。