こんにちは、かつコーチです。
他人が書いたPythonのコードを読んでいて、lambda x: x * 2のような記述に出くわし、手が止まった経験はないでしょうか。
「lambdaって何」
「mapやfilterは内包表記と何が違うの」
そう感じている方も多いと思います。
この記事では、ラムダ式の基本構文から、map・filter・functools.reduceの使い方、そして内包表記との使い分けまでを解説します。
読み終える頃には、他人のコードに出てくるlambdaを読めるだけでなく、場面に応じて使い分けられるようになります。
ラムダ式の基本構文
lambdaとは名前を持たない関数
ラムダ式とは、defを使わずにその場で定義できる、名前を持たない小さな関数です。
「無名関数」とも呼ばれます。
add = lambda a, b: a + b
print(add(2, 3))
実行結果は次の通りです。
5
defで書くと次のようになります。
def add(a, b):
return a + b
lambda 引数: 返り値の式という1行だけで、同じ内容を表現できるのがラムダ式の特徴です。
lambdaが向いている場面
ラムダ式は、defで名前をつけるほどでもない、一度きりの短い処理に向いています。
代表的なのが、後述するsortedのkey引数やmap・filterの第1引数として、その場で渡す用途です。
words = ["banana", "fig", "apple", "kiwi"]
sorted_words = sorted(words, key=lambda w: len(w))
print(sorted_words)
実行結果は次の通りです。
['fig', 'kiwi', 'banana', 'apple']
key=lambda w: len(w)という形で、「文字列の長さ」を並び替えの基準として、その場で渡しています。
わざわざdef get_length(w): return len(w)と定義するまでもない、こうした場面でラムダ式が活躍します。
map・filterの使い方
mapで全要素を変換する
mapは、リストなどの各要素に対して、同じ処理を一括で適用する関数です。
numbers = [1, 2, 3, 4, 5]
doubled = map(lambda x: x * 2, numbers)
print(list(doubled))
実行結果は次の通りです。
[2, 4, 6, 8, 10]
map(関数, リスト)という形で呼び出すと、リストの各要素に関数を適用した結果を返します。
ここで注意したいのが、mapが返すのはイテレータ(値を1つずつ順番に取り出せるオブジェクト)であり、リストそのものではない点です。
中身を確認したり、そのままリストとして使ったりしたい場合は、list()で変換する必要があります。
filterで条件に合う要素だけ残す
filterは、条件に一致する要素だけを取り出す関数です。
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers))
実行結果は次の通りです。
[2, 4, 6, 8, 10]
filter(関数, リスト)の関数部分は、TrueかFalseを返す必要があります。
Trueが返った要素だけが、結果として残る仕組みです。
mapと同じく、filterが返すのもイテレータなので、中身を見るにはlist()で変換します。
functools.reduceの使い方
reduceで要素を1つにまとめる
reduceは、リストの要素を左から順に処理し、最終的に1つの値にまとめる関数です。
標準ライブラリのfunctoolsモジュールからインポートして使います。
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)
print(total)
実行結果は次の通りです。
15
lambda acc, x: acc + xのaccは「これまでの累積結果」、xは「今処理している要素」を表します。
1 + 2 = 3、3 + 3 = 6、6 + 4 = 10、10 + 5 = 15という順で計算が進み、最終的に15が返ります。
合計を求めるだけならsum()で十分ですが、最大値の比較や文字列の連結など、独自の集約ルールを組みたいときにreduceが役立ちます。
初期値を指定する
reduceには第3引数として初期値を指定できます。
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers, 100)
print(total)
実行結果は次の通りです。
115
初期値100を起点として、100 + 1 + 2 + 3 + 4 + 5 = 115という計算になります。
空のリストを渡す可能性がある場合は、初期値を指定しておかないとエラーになるため注意が必要です。
よくあるつまずきポイント・エラー対処
空リストでreduceがエラーになる
筆者が実務で集計処理を書いていたとき、対象データが0件のケースを想定しておらず、本番相当の環境でエラーに遭遇したことがあります。
❌ Before(初期値なしでエラーになる書き方)
from functools import reduce
numbers = []
total = reduce(lambda acc, x: acc + x, numbers)
print(total)
実行すると、次のエラーメッセージが表示されます。
TypeError: reduce() of empty iterable with no initial value
reduceは、リストが空で初期値もない場合、集約を開始する基準がなく、エラーを出す仕様になっています。
✅ After(初期値を指定した書き方)
from functools import reduce
numbers = []
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total)
実行結果は次の通りです。
0
初期値0を指定しておくことで、リストが空でもエラーにならず、0が返るようになります。
集計処理を書くときは、「対象データが0件だったらどうなるか」を必ず確認する習慣をつけましょう。
内包表記との使い分け
同じ処理を内包表記で書き換える
mapとfilterは、リスト内包表記でも同じ処理を書けます。
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# mapの代わり
doubled = [x * 2 for x in numbers]
# filterの代わり
even_numbers = [x for x in numbers if x % 2 == 0]
print(doubled)
print(even_numbers)
実行結果は次の通りです。
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
[2, 4, 6, 8, 10]
見比べると分かる通り、単純な変換や絞り込みであれば、内包表記の方が1行で読みやすく書けます。
判断軸:どちらを選ぶべきか
| 場面 | おすすめ |
|---|---|
| 単純な変換・絞り込み | 内包表記 |
| 変換と絞り込みを同時に行う | 内包表記(ifとforを組み合わせる) |
| 既にある関数を適用したい | map・filter |
| 要素を1つの値に集約したい | functools.reduce |
| Pythonの一般的なコーディング規約に沿いたい | 内包表記を優先 |
Pythonの開発者コミュニティでは、可読性の観点から、単純な変換や絞り込みには内包表記を使うことが推奨される傾向にあります。
一方で、既に定義済みの関数(自作関数やstrのメソッドなど)をそのまま適用したいだけの場合は、map(str.upper, words)のように書けるmapの方がシンプルなこともあります。
reduceに相当する処理は内包表記では書けないため、集約が必要な場面ではreduceか、通常のforループを使うことになります。
まとめ
この記事のポイント
- ラムダ式は
lambda 引数: 返り値の式で書く、名前を持たない小さな関数である mapは全要素の変換、filterは条件に合う要素の絞り込みに使うmap・filterの結果はイテレータなので、中身を見るにはlist()で変換するfunctools.reduceは要素を1つの値に集約する。空リストでは初期値の指定が必須になる- 単純な変換・絞り込みは内包表記、集約が必要な場合は
reduceを使うのが基本の使い分けである
次に読むべき記事
- リスト内包表記の書き方
- ジェネレータとyieldの使い方
- collections.Counterでデータを集計する
タグ: Python, 中級者向け, 基本文法