今川館

都内勤務の地味OLです

リストの隣接要素を次々に処理する

map_between をやってみる - Study08.net 対シンバシ殲滅用人型機動兵器
こういう記事があって、リストの隣接する要素を次々に処理する高階関数を作る話のようです。

期待する挙動としては、こんな感じです。

>>> map_between(lambda x, y: x + y, [1, 2, 3, 4, 5])
[3, 5, 7, 9]

わたしも試しに書いてみました。

# -*- coding:utf-8 -*-
import itertools


# お手軽な方法だが、スライスしてしまう。
# スライシングに応答できるオブジェクトじゃないと動かない。
map_between1 = lambda func, lst: itertools.imap(func, lst, lst[1:])


def map_between2(func, lst):
    """ジェネレータを使う方法。この記事ではこっちを主張したい。"""
    itr = iter(lst)
    x = itr.next()
    for y in itr:
        yield func(x, y)
        x = y


def main():
    add = lambda x, y: x + y
    lst = range(1, 6)
    
    L1 = list(map_between1(add, lst))
    print(L1) #=> [3, 5, 7, 9]
    
    L2 = list(map_between2(add, lst))
    print(L2) #=> [3, 5, 7, 9]


if __name__ == "__main__":
    main()

スライシングしたくない

map_between1の方はささっと書くには手軽です。
しかし、リストの隣接要素を次々に処理する為に、

itertools.imap(func, lst, lst[1:])

こういう風にスライシングを使わないで同じことを実現したいと考えました。

理由は二つあります。

  1. 引数lstがスライシングに応答しないとエラーになってしまうので、それを気にする必要の無い書き方にしたい。
  2. スライシングすると別のオブジェクトを作って返す。仮に巨大なリストを引数に渡したときにそのリストの全長-1の大きさのリストを複製するのは(大したコストじゃないんだろうけど)嫌だ。

ジェネレータにしておけば、要素を次々に反復するだけで済みます。
それがmap_between2の方です。

iter関数を使ってイテレータを操作する

また、単にforやwhileで要素を反復するのではなく、今回は「次」と「次の次」をまとめて反復したいので、next関数を使いました。

但し、listはnext関数の引数に渡すとエラーになってしまうので、iter関数を使ってイテレータを操作することにしました。

>>> next([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list object is not an iterator

>>> next(iter([1, 2, 3]))
1

イテレータプロトコルって何?

さっき言った通り、listはnextに渡せません。
pythonには「イテレータプロトコル」というものがあって、

  1. next関数に渡せる → nextまたは__next__という名前のメソッドを持っている。(__next__はpython3のルール)
  2. iter関数に渡せる → __iter__という名前のメソッドを持っている。

このルールを守ることらしいです。
この意味において、listはイテレータプロトコルを守っていません。

詳しくはこの辺りを見ればいいのでしょうか。
4. 組み込み型 — Python 3.4.2 ドキュメント
PEP 234 - PEP 234 -- Iterators | Python.org

最後に

あと、引用元の記事では大元の記事(JavaScriptのコード)と同じ利用の仕方ができるよう、listを継承したListというクラスを作るコードも書いてありましたが、わたしはやらないことにしておきます。

追記: 2012/01/07

map_between1を最初このように書いていました。

map_between1 = lambda func, lst: (func(x, y) for x, y in itertools.izip(lst, lst[1:]))

これについてcocoatomoさんから指摘を頂いて修正しました。

map_between1 = lambda func, lst: itertools.imap(func, lst, lst[1:])

わたしのリプライの通り、ジェネレータ式とizipを組み合わせるのはややこしいだけなので、元々のtell-kの記事の書き方(imapを使う上記方法)が簡潔で良いですね。