入力された数字を、文字列から整数にまとめて変換したい。そんな場面で、forループと空のリストを用意して1つずつappendしていませんか。
もちろんそれでも正しく動きます。ただ、ほかの人のコードを読んでいると、map()やfilter()という見慣れない関数に出会うことがあるはずです。
map()は全要素に同じ処理をする関数で、filter()は条件に合う要素だけを残す関数です。どちらもimportなしで使える組み込み関数なので、知っておくとコードを読む力がぐっと上がります。
今回は、2つの関数の基本から、リスト内包表記との使い分け、つまずきやすい落とし穴まで紹介します。手元のPython 3.11と3.14で動かした結果を載せているので、一緒に確かめながら読んでみてください。
map()とfilter()は何をする関数か¶
まずは、2つの関数の役割をざっくり押さえておきましょう。どちらも、リストのようにたくさんの値をまとめて扱うときに使います。
map()は、渡した関数をすべての要素に順番に当てはめる関数です。たとえば、全部の数字を2倍にする、全部の文字列を大文字にする、といった処理に向いています。
filter()は、渡した関数がTrueを返した要素だけを残す関数です。偶数だけを取り出す、空の文字列を取り除く、といった絞り込みに使います。
2つの違いを、表で並べてみました。
| 関数 | 役割 | 要素の数 | 渡す関数が返すもの |
|---|---|---|---|
| map() | 全要素を変換する | 元と同じ | 変換後の値 |
| filter() | 条件に合う要素だけ残す | 元と同じか少ない | TrueかFalse |
map()は数が変わらず中身が変わり、filter()は中身が変わらず数が減る、と覚えるとわかりやすいですね。この違いさえつかめば、どちらを使えばよいか迷うことは少なくなります。
map()の基本的な使い方¶
ここからは、実際にコードを動かしながら見ていきましょう。最初は、初心者がいちばん出会いやすい文字列から整数への変換です。
文字列のリストを整数に変換する¶
input()で受け取った値は、すべて文字列になります。スペース区切りの数字をまとめて整数にしたいときは、次のように書けます。
text = "10 20 30"
numbers = list(map(int, text.split()))
print(numbers) # [10, 20, 30]
map()の1つ目の引数には関数、2つ目にはリストなどを渡します。ここではint関数を、split()で分けた3つの文字列それぞれに当てはめています。
ポイントは、intのうしろにかっこを付けないことです。int()と書くとその場で呼び出してしまうので、関数そのものを渡すつもりで名前だけを書きます。
同じ処理をforループで書くと、次のようになります。
text = "10 20 30"
numbers = []
for s in text.split():
numbers.append(int(s))
print(numbers) # [10, 20, 30]
4行かかっていた処理が、map()なら1行で済みましたね。競技プログラミングの入力処理でよく見かけるのも、この書き方です。
lambdaと組み合わせて自分の処理を当てはめる¶
int()やstr()のような既存の関数がないときは、lambdaで小さな関数をその場で作って渡せます。全部の値を2倍にする例を見てみましょう。
prices = [100, 250, 480]
doubled = list(map(lambda x: x * 2, prices))
print(doubled) # [200, 500, 960]
lambda x: x * 2は、xを受け取ってx * 2を返す関数という意味です。名前を付けるほどでもない使い捨ての処理に向いています。
lambdaの書き方にまだ自信がない方は、先にこちらで基本を確認しておくと読みやすくなります。【関連記事】ラムダ式(無名関数)を使いこなす。コードを極限までシンプルにする方法を解説
filter()の基本的な使い方¶
続いてfilter()です。こちらは、条件に合う要素だけを取り出したいときに使います。
偶数だけを残す例を書いてみました。
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, numbers))
print(evens) # [2, 4, 6]
lambdaの部分が、残すかどうかを決める判定役です。nを2で割った余りが0ならTrueになるので、偶数だけが結果に残ります。
Noneを渡すと偽の値をまとめて取り除ける¶
filter()には、ちょっと便利な使い方があります。1つ目の引数に関数ではなくNoneを渡すと、偽とみなされる値がまとめて取り除かれるのです。
values = [0, 1, "", None, "a", []]
print(list(filter(None, values))) # [1, 'a']
0や空の文字列、None、空のリストが消えて、1と"a"だけが残りました。CSVを読み込んだあとに空のセルを捨てたいときなどに、さっと使えて重宝します。
ただし、0も一緒に消えてしまう点には注意してください。数値の0を正しいデータとして残したい場面では、filter(lambda v: v is not None, values)のように条件をはっきり書くほうが安全です。
結果が一度しか使えないイテレータの落とし穴¶
ここからが、今回いちばん伝えたい内容です。map()やfilter()の結果をprint()でそのまま表示すると、思っていたのと違うものが出てきます。
m = map(str, [1, 2, 3])
print(m) # <map object at 0x7f...>
print(list(m)) # ['1', '2', '3']
print(list(m)) # []
1行目の表示は、リストではなくmapオブジェクトというものです。Python 3のmap()とfilter()は、リストではなくイテレータを返すようになっています。
イテレータは、値を1つずつ取り出すための仕組みです。取り出し終わったら空っぽになるので、3行目のように2回目のlist()では空のリストが返ってきます。
実務でハマった集計が0件になる不具合¶
私は10年ほどエンジニアとして開発に関わってきましたが、このイテレータの性質で半日ほど悩んだことがあります。filter()で絞り込んだ結果を、件数のチェックと集計の2か所で使っていたところ、集計のほうだけ毎回0件になっていました。
原因は、件数のチェックでlist()に変換した時点で、filterオブジェクトの中身を使い切っていたことでした。エラーも出ずに静かに0件になるので、なかなか気づけなかったのです。
それ以来、2回以上使う結果は、最初にlist()でリストにしてから変数に入れるようにしています。書き方は次のとおりです。
orders = [1200, 0, 3500, 800]
valid = list(filter(lambda x: x > 0, orders))
print(len(valid)) # 3
print(sum(valid)) # 5500
逆に言えば、1回だけ使うならイテレータのままでも問題ありません。必要になったときに1件ずつ計算するので、大きなデータでもメモリを節約できるのが長所です。
値を1つずつ作る仕組みはジェネレータと同じ考え方なので、こちらの記事も参考になります。【関連記事】ジェネレータ(yield)っていつ使うの?巨大なデータをメモリ節約して扱う方法
リスト内包表記とどちらを使うべきか¶
ここまで読んで、それってリスト内包表記でも書けるのでは?と思った方もいるかもしれません。実はそのとおりで、map()とfilter()はどちらも内包表記で書き換えられます。
同じ処理を並べて比べてみましょう。
numbers = [1, 2, 3, 4, 5, 6]
# map()とfilter()を組み合わせた書き方
a = list(map(lambda n: n * 10, filter(lambda n: n % 2 == 0, numbers)))
# リスト内包表記の書き方
b = [n * 10 for n in numbers if n % 2 == 0]
print(a) # [20, 40, 60]
print(b) # [20, 40, 60]
結果は同じですが、読みやすさはずいぶん違いますね。lambdaが2つ重なる書き方より、内包表記のほうが左から右へ素直に読めます。
Python 3.0のリリース時の公式ドキュメントでも、map()の結果をlist()で包む代わりに、内包表記を使うかコードを見直すことが勧められています。Pythonの世界では、lambdaが必要になる場面は内包表記を選ぶのが一般的です。
使い分けの目安を、表にまとめてみました。
| 場面 | おすすめ | 理由 |
|---|---|---|
| 既存の関数をそのまま当てはめる | map() | map(int, items)のように短く書ける |
| lambdaを書く必要がある | 内包表記 | 式をそのまま書けて読みやすい |
| 変換と絞り込みを両方したい | 内包表記 | forとifを1行にまとめられる |
| 偽の値をまとめて捨てたい | filter(None, ...) | 意図が短く伝わる |
| 結果を1回だけ順番に使う | map()かジェネレータ式 | リストを作らずメモリを節約できる |
迷ったら内包表記を選び、int()やstr.strip()のような既存の関数を当てはめるだけのときにmap()を使う。このくらいの感覚で十分です。
内包表記の書き方をくわしく知りたい方は、こちらの記事でじっくり解説しています。【関連記事】Pythonのリスト内包表記を使いこなせ!3行のループを1行にまとめる書き方
複数のリストを渡すmap()とstrict引数¶
map()には、リストを2つ以上渡せるという特徴もあります。このとき、関数には各リストから1つずつ取り出した値が同時に渡されます。
prices = [100, 200, 300]
counts = [2, 5, 1]
totals = list(map(lambda p, c: p * c, prices, counts))
print(totals) # [200, 1000, 300]
単価と個数のリストから、それぞれの小計を一気に計算できました。zip()で組にしてから計算するのと同じことを、1回で済ませている形です。
ここで気をつけたいのが、リストの長さがそろっていないときの動きです。map()は短いほうのリストが終わった時点で止まり、残りの要素は黙って無視されます。
Python 3.14で追加されたstrict引数¶
この黙って無視される動きは、データの取りこぼしに気づきにくい原因になります。そこでPython 3.14では、map()にstrictというキーワード引数が追加されました。
strict=Trueを付けると、長さがそろっていないときにValueErrorが出るようになります。手元のPython 3.14で試した結果は次のとおりです。
a = [1, 2, 3]
b = [10, 20]
print(list(map(lambda x, y: x + y, a, b))) # [11, 22]
print(list(map(lambda x, y: x + y, a, b, strict=True)))
# ValueError: map() argument 2 is shorter than argument 1
同じ仕組みは、Python 3.10からzip()にも用意されています。長さがそろっているはずのデータを組み合わせるときは、strict=Trueを付けておくと安心です。
ただし、Python 3.13以前ではstrict引数は使えずエラーになります。チームで使っているPythonのバージョンを確認してから取り入れてください。
zip()の基本的な使い方は、こちらの記事で紹介しています。【関連記事】Pythonのzip関数の使い方を解説!複数のリストをスマートにまとめる技
reduce()がfunctoolsにある理由¶
map()とfilter()を調べていると、reduce()という関数の名前もよく一緒に出てきます。reduce()は、要素を左から順に1つの値へたたみ込んでいく関数です。
Python 2では組み込み関数でしたが、Python 3.0からはfunctoolsモジュールへ移されました。使うときは、次のようにimportが必要です。
from functools import reduce
numbers = [1, 2, 3, 4]
product = reduce(lambda a, b: a * b, numbers)
print(product) # 24
1×2×3×4を計算して、24が返ってきました。とはいえ、合計ならsum()、最大値ならmax()のように、専用の関数があるときはそちらを使うほうが読みやすくなります。
functoolsには、ほかにもキャッシュを効かせるlru_cacheなど便利な道具がそろっています。興味のある方は、こちらもあわせて読んでみてください。【関連記事】Pythonのfunctoolsとは?lru_cacheやpartialで関数を速く短くする使い方を初心者向けに解説
まとめ¶
map()は全要素に同じ処理を当てはめる関数で、filter()は条件に合う要素だけを残す関数です。どちらも組み込み関数なので、importなしですぐに使えます。
結果はリストではなくイテレータで返ってくるので、一度使うと空っぽになります。2回以上使うなら、最初にlist()でリストにしておくのが鉄則です。
lambdaを書く必要があるときは、リスト内包表記のほうが読みやすくなることが多いです。map(int, items)のように既存の関数を当てはめる場面では、map()の短さが光ります。
まずは、手元のコードにある文字列を整数に変換するループを、1か所だけmap()に書き換えてみてはいかがでしょうか。読み返したときに、すっきりしたと感じられるはずです。
ここまでお読みいただきありがとうございました。