読者です 読者をやめる 読者になる 読者になる

今川館

都内勤務の地味OLです

イテレータって何?

Python

iter関数にリストを渡すとリストはコピーされないの?

昨日書いた↓この記事を見たtell-kがコメントをつけてくれました。
http://d.hatena.ne.jp/imagawa_yakata/20120104/1325703433

> itr = iter(lst)

これってイテレータオブジェクトになったコピーが返ってくるんじゃなかろうかと勝手に思ってるんですが、その場合 2番目の件と同じような気がするのですが、そんな事はないんでしょうか。さーせん。よくわかってなす。


2番目の件 => スライシングすると別のオブジェクトを作って返す。

イテレータとリスト

結論から言うと、イテレータはリストの参照を持っているだけで、イテレータオブジェクトが沢山作られてもリストの方を複製したりしないので大丈夫です。
(一応、拙いC言語の能力でlistobject.cを読んでみましたが、この認識で間違いなさそうです。)

イテレータとは、リストのような複数の要素を持つオブジェクトを前から順に一つずつ参照していく為の道具です。
リストとイテレータは別もので、なおかつイテレータはリストの複製ではなく、参照を持っていればいいだけです。

そしてイテレータは大抵、「次の要素」を取り出すメソッドを持っています(=nextのこと)。
前の記事でリストの隣接要素を得るにあたっては、「次」と「次の次」をまとめて参照できれば良くて、イテレータはまさにこの要請に応えてくれます。

何か、言葉を尽くせば尽くすほどわかりにくくなるので、pythonのコードを書いて説明してみます。

# -*- coding:utf-8 -*-


class ListLike(object):
    def __init__(self, *args):
        self.values = args

    def __iter__(self):
        return ListIterator(self)

    def __getitem__(self, index):
        return self.values[index]


class ListIterator(object):
    def __init__(self, lst):
        self.lst = lst 
        self.idx = -1

    def __iter__(self):
        return self

    def next(self):
        self.idx += 1
        try:
            return self.lst[self.idx]
        except IndexError:
            raise StopIteration


def main():
    lst = ListLike("ham", "and", "jam", "and", "spam", "a lot")
    itr = iter(lst)
    for x in itr:
        print(x)
    # イテレータは同じリストへの参照を持っているだけであることを確認する。
    assert itr.lst is iter(lst).lst
    # イテレータオブジェクト自体は別ものである。
    assert itr is not iter(lst)


if __name__ == "__main__":
    main()

mainの中の

# イテレータは同じリストへの参照を持っているだけであることを確認する。
assert itr.lst is iter(lst).lst
# イテレータオブジェクト自体は別ものである。
assert itr is not iter(lst)

このassert文ですが、これでリストをコピーしていないことが確認できると思います。

map関数はiterableを期待するのでmap_betweenも合わせよう

map関数は、引数に渡されたiterableなオブジェクトのすべての要素に、もう一つの引数に渡された関数を適用した結果をリストで返します。

一応、pythonのドキュメントを引用すると、
2. Built-in Functions — Python 2.7.9 documentation

The iterable arguments may be a sequence or any iterable object; the result is always a list.

戻り値は常にlistだが、引数はiterableを期待すると書いてあります。

つまり、iterableである(=次々と要素を返してくれる)ことを期待するのであって、listを期待するわけではありません。
iterableならば、リストでも、タプルでも、文字列でも、ジェネレータでも何でもいいのです。

mapと同じインターフェースのmap_betweenという関数を作るのならば、どんな引数だったら動くかという条件は合わせておいた方が良いです。
mapにはジェネレータも渡せるのに、mapの兄弟分のmap_betweenに渡すとスライシングできなくてエラー、というのは簡単に回避できるなら回避したい問題です。

GoFIteratorパターンの定義にある、

集約オブジェクトが基にある内部表現を公開せずに、その要素に順にアクセスする方法を提供する。

まさにこういうケースですね。
iter関数を使ってイテレータを使うとgoodというわけです。

これで納得してもらえましたかね、tell-kさん。

追記

思えばmap_between2の引数はiterableだが、戻り値がlistではなくジェネレータになっているので厳密にはmapと足並みが揃っていませんね。
そして、listで結果を返すとなると、ロジックを考え直す必要がありそうですね。

気が向いたらやろうかと思います。