minminの備忘録

フルスタックになりたいエンジニアの備忘録 → https://zuminblog.com/ へ引越し中

分かってるようで分かってないラムダ式について

コードを書く時だったり、コードレビューをする時にラムダ式を見かけることがちょくちょくあるのですが、毎回こんな感じだろうなって感じで曖昧な理解のままで素通りしていたので、今回はラムダ式をちゃんと理解しようということで、まとめてみました。

ラムダ式とはなんぞや?という人の助けになれば幸いです。

ラムダ式とは

ラムダ式とは、

名前が定義されていない関数を記述するための方法

です。

???って感じですよね。。 (私も最初はこの表現じゃよくわかりませんでした。。)

ということで、早速コードを見ていきましょう。今回はpythonでコードを書いていきます。

ラムダ式の書き方

まずは、基本的な書き方についてです。

(関数名 = )lambda 引数1, 引数2, ...:何らかの処理

print(関数名(引数1, 引数2, ...))と記述すると、処理された結果が出力されます。

※注意事項

lambda式で関数名を定義するのは、PEP8(pythonのコーディング規約)で推奨されていません。

上で「名前が定義されていない関数を記述するための手法」と言っているのに、関数名を定義するのはおかしな話ですよね笑

具体的には、以下のサイトに記載されています。(lambdaでページ内検索すると出てくると思います。)

www.python.org

コード例

掛け算

まずは簡単な例からということで、与えられた2つの変数の積を求める式を、defで関数名を定義した場合とラムダ式を使った場合を比較したいと思います。

#defで関数名を定義した場合
def multiple(a, b):
  return a*b
#ラムダ式
lambda a, b: a*b

うーん、これじゃまだlambda式のうまみが分かりづらいですね、、、

三項演算子を使ってみる

ただ引数を乗算するだけじゃ面白くないので、簡単な条件分岐を使ってみましょう

#defで関数名を定義した場合
def is_over_100(a):
    return True if a > 100 else False
#ラムダ式
lambda a: True if a > 100 else False

個人的には、まだうまみがわからないですね、、

そもそも上記の利用方法でlambdaで定義した関数を使うためには、関数名を定義しなければいけないので、lambda式を使う理由がないですね。。

map関数に適用

ここでよく使われている(個人的な観測範囲内ですが)、map関数にlambda式を組み込む方法についてみていきましょう。

map関数とは、リストなどのイテラブルなオブジェクトの各要素に対して、第一引数に指定した関数を実行する関数です。返すのはイテレーターなので、実際に出力するときはlistにキャストする必要があります。

#書き方
map(適用したい関数, イテラブルなオブジェクト)
#例
sample_list = [1,2,3,4]
print(list(map(str, sample_list))) #['1', '2', '3', '4']

以下がdefラムダ式をmap関数に適用した例です。

sample_list = [1, 2, 3, 4, 5]
#defで関数名を定義した場合
def square_only_even(x):
    return x**2 if x%2==0 else x
print(list(map(square_only_even, sample_list))) #[1, 4, 3, 16, 5]
#ラムダ式
print(list(map(lambda x:x**2 if x%2==0 else x, sample_list))) #[1, 4, 3, 16, 5]

ちょっとコード量が減りました!
しかし、以下のように拡張for文を使えば、lambda式と同じようにかけてしまうので、う〜ん…って感じですね。むしろ拡張for文の方がすっきりしてる気がします

print([x**2 if x%2==0 else x for x in sample_list]) #[1, 4, 3, 16, 5]

ラムダ式が威力を発揮する場面

そこで個人的にうまみがあると思った方法について、紹介したいと思います

sorted関数に適用

sorted関数とは、イテラブルなオブジェクトをソートする関数で、keyとして「何を持ってソートするか」ということを指定することができます。

以下の例は、各要素の2番目を比較してソートするコードなんですが、ここでlambda式が使われています。

sample_list = [[12,3],[10,1],[6,2],[9,6],[1,9]]
#defで関数名を定義した場合
def get_2nd_number(x):
    return x[1]
print(sorted(sample_list, key=get_2nd_number)) #[[10, 1], [6, 2], [12, 3], [9, 6], [1, 9]]
#ラムダ式
print(sorted(sample_list, key=lambda x:x[1])) #[[10, 1], [6, 2], [12, 3], [9, 6], [1, 9]]

map関数で見た時と同様に、関数を定義していない分コード量は減っています。

この場合は他の良い書き方があまり思いつかなかったので、lambda式を使う場面かなと思っています。

min/max関数に適用

min/max関数とは、無名引数が1つのイテラブルなオブジェクトの場合、その中で最小/最大の要素を返す関数です。

(2つ以上の無名引数が与えられた場合は、それらの中から最小/最大の要素を返します。)

min/max関数にlambda式を適用した例は、以下になります。

sample_list = [[12,3],[10,1],[6,2],[9,6],[1,9]]
#defで関数名を定義した場合
def get_2nd_number(x):
    return x[1]
print(max(sample_list, key=get_2nd_number)) #[1, 9]
#ラムダ式
print(max(sample_list, key=lambda x:x[1])) #[1, 9]

これも、sorted関数で見た時と同様で言わずもがなですね。

メリット・デメリット

メリット

  • 扱う変数名が減る

扱う変数名が減ることで、名前衝突のリスクが減りコードが管理しやすくなります。

  • 関数名を定義する場合より、コード量が減る(一般的には)

def hoge(a,b):...return c の部分を書かなくてよくなるので、一般的にはコード量が減り可読性が上がります。

もちろん書き方によってはコード量は増えますし、コードの可読性が下がります。

デメリット

  • コードが見にくくなることがある

以下のような例のラムダ式だと、(個人的には)可読性の悪いコードを生み出しているように感じます。

sample_list = [1, 2, 3, 4, 5]
sample_list2 = [1,4]
print(list(map(lambda x:x**2+10 if x not in sample_list2 and x%2==0 else x**3, sample_list))) #[1, 14, 27, 64, 125]

コードは他人が読めるように書くものなので、処理が長くなってしまうような関数を使用したい場合は、以下のようにdefで定義してあげた方が、見やすくなることもあります。

def sample_func(x):
    if x not in sample_list2 and x%2==0:
        return x**2 + 10
    else:
        return x**3
print(list(map(sample_func, sample_list))) #[1, 14, 27, 64, 125]

もちろん場合によりけりなので、どちらの選択肢も持っておいた上で選択できるようにしておくのがベストです。

参考