静かなる名辞

pythonとプログラミングのこと


map・filterとリスト内包表記はどちらを使うべきか?

はじめに

 pythonにはmap・filterという関数と、リスト内包表記という独自の記法があります。

 どちらを使っても同じようなことができますが、どちらを使うべきなのでしょうか?

 色々な視点から考えてみます。

 目次


スポンサーリンク


返り値の型

 map・filterにはmapオブジェクト、filterオブジェクトというジェネレータのようなものを返すという厄介な性質があります。ただしこれはpython3以降の仕様なので、python2を使っている方には当てはまりません(python3とpython2のソースコード互換性に効いてくるので、それはそれで難しい問題ではある)。

 リスト内包表記でジェネレータの返り値を望むのならジェネレータ式を使うことができますが、map・filterでリストを返したければlist()を使ってリストに変換するしかありません。

>>> [x*2 for x in range(10)]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> list(map(lambda x:x*2, range(10)))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

 list()は厄介で、何しろ6文字も余計なものが増えてしまいます。ソースコードが冗長になり、list()が増えすぎると可読性も損ないます。

 なお、star operatorを使って3文字で済ませる裏技もあります。これができるのは比較的新しいバージョンのpythonだけのはずですが・・・。

>>> [*map(lambda x:x*2, range(10))]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

 可読性は悪いです(というか初見殺し。そしてこの書き方は流行っていない)。

冗長さ

 上述の通り、map・filterはリストに変換してやる必要があるので、基本的に冗長です。lambdaが必要になるとなおさら、という気がします。再掲しますが、

>>> [x*2 for x in range(10)]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> list(map(lambda x:x*2, range(10)))
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

 リスト内包表記の方は24文字、mapの方は34文字(どちらもスペース込み)ですから、勝ち目はなさそうです。

 しかし、「返り値はジェネレータのようなもので構わない(もしくは積極的にジェネレータを使いたい)」かつ「既存の関数を使い、lambdaを使わない」という条件だと、mapも輝き始めるようです。数字の文字列を一文字ずつintのリストに変換する例。

>>> (int(c) for c in "1234")
<generator object <genexpr> at 0x7fefabac23b8>
>>> map(int, "1234")
<map object at 0x7fefaba4b0f0>

 上は24文字、下は16文字です。内包表記の「for * in」がなくなるのと、呼び出しを省けるので短くなります。

 filterも事情は同様です。よって、冗長さについては「ケースバイケース」であり、より詳しく言うと「返り値がジェネレータで構わず、使う関数がすでに定義されている」場合はmap・filterが有利と言い切れます。逆に言うと、それ以外のケースでは内包表記の方が簡潔に書けるようです。

 たとえば、入力された数値n個をint型に変換する、といったコードでは、

a,b,c = map(int, input())

 のように、極めて簡潔な記述がmapを使うことで可能になります。

可読性

 可読性ははっきり言って、一長一短です。

 一般的なプログラミング言語の構文をベースに考えると、わかりやすいのはmap・filterの方で、何しろ普通の関数です。初心者でも安心して使えると言えます(無名関数と高階関数の概念さえ理解すれば)。ただし、現実的には増えまくるlist()のせいでコードがごちゃごちゃし、かえって可読性が下がります。また、mapとfilterはそれぞれ独立に使う必要があるので、なおさらコードがごちゃごちゃします。

 リスト内包表記は一見するとわかりづらいですが、慣れてしまえば簡潔で、書きやすく、読みやすいと言えます(本当か? 個人差は確実にあると思う)。また、一つの内包表記でfilterとmapを同時に適用できるので簡潔になります。

 多重の複雑なものになると、そもそも簡潔に書け、改行とインデントでかなりわかりやすく整理できる内包表記の方が相対的にかなり有利と感じています。

from pprint import pprint
pprint([[x*y for y in range(1,10)]
        for x in range(1,10)])

pprint(list(map(
    lambda x:list(map(
        lambda y:x*y,
        range(1,10))), 
    range(1,10))))

 かけ算九九の表を表示するプログラムですが、mapの方ははっきり言ってひでーと思います。この程度ならまだ読めなくはありませんが、条件が加わってfilterを追加するとかやり始めるともはや読めなくなります。

事故

 これはmap・filterの欠点というよりはジェネレータの欠点ですが、割と事故のもとになってくれます。

>>> result = map(lambda x:x*2, range(10))
>>> result[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'map' object is not subscriptable
>>> for x in result:
...     print(x)
... 
0
2
4
6
8
10
12
14
16
18
>>> for x in result:
...     print(x)
... 
>>> # 何も表示されない

 「んなもん、ちゃんと理解して使えば良いだろ」という意見も当然あると思います。それはそれで正しいと思いますが、理想論です。私は自分自身を含めて人間を100%信頼はしていません。事故る要素があれば、必ず事故るのです。

 そして、これが嫌という理由で私は普段、ほとんどリスト内包表記を使っています。*1

まとめ

 基本的には内包表記がずいぶん有利です。特別な理由がなければ内包表記で書けば良いと思います。

 map・filterはリストに既存の関数を適用するジェネレータを作るときに威力を発揮する可能性があります。あるいは、代入でunpackするときとか。

 というか、それ以外なさそうです。無名関数と組み合わせてまで使う必然性はないと思います。

*1:余談ですが、私はpythonのジェネレータというものはあまり好きではありません。mapやfilterやzip, rangeなどの返り値は、基本的にすべてlistでも別に良いと思います。これらの関数にはgeneratorオプションを追加してdefault=Falseとするか、python2よろしく対応するxrangeなどを作る、あるいはジェネレータを示す糖衣構文を新しく作って、その糖衣構文で囲むと値の評価時にジェネレータとして処理されるような仕組みを導入すれば良いと思います。これらについては、python4に期待です。