LoginSignup
1
3

More than 5 years have passed since last update.

pythonでnextメソッドを動的に置き換えるには

Last updated at Posted at 2017-07-24

pythonのfor文の挙動について勘違いしているかもしれないので、理由に心当たりがありましたら教えていただけるとうれしいです。解決しました。

準備

同じ文字列を3回返すイテレータ

iter.py
# 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

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

参考リンク

1
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3