~~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
メソッドを置き換えるなんてことを試していたらこの問題に遭遇してしまった。