[Work/Class/Python3の基礎とデータ処理/1_PythonBasic]

lambda式, 高階関数, 内包表記

注意

このページは,Pythonの言語仕様の最大の特徴である部分を取り上げているのだが,NumPyなどの数値計算ライブラリには使えない(使うと遅くなる)ので,ぶっちゃけ覚えなくても良い

高階関数

map()関数,filter()関数など,関数そのもの(lamda式で定義する)を引数に取ったり,戻り値に指定する関数のことを「高階関数」と呼ぶ.関数型言語ではよく使われるものである.逆にC言語やJava等関数が必ず静的に存在していなければならない言語では使うことができない.

lambda式

lambda式とは,(Pythonでは)1行で表現でき,かつ何らかをreturnする関数を,その場で定義して使うものである.一般的には「lambda関数(無名関数)」と呼ばれるのだが,Pythonのそれは「returnする値を生成する1行のみの式」という縛りがあるため「lambda式」と呼ばれている.

lamda式は関数オブジェクトを生成する.

JavaScriptでコールバック関数に,その場で定義する関数を渡すのと同じ形である.

print((lambda x, y: x + y)(3, 7))

何を行っているかわかんねーと思うが(AA略,lambdaというキーワードに続けて「このlambda式の引数はx, y」であるという情報,returnする値を生成する処理の中身「x + y」,そのlambda式に,引数3と7を与えて呼び出す,という構成になっている.出力されるのは3+7で「10」になる.

lambda式の中でif〜elseを使いたい場合には,後ろに書くという決まりになっているが,その順番がややこしい.

lambda 引数: Trueの時に返す値を作る式 if 条件式 else Falseの時に返す値を作る式

という順番になる.

例えば「第1引数が第2引数より大きい場合は第1引数の2倍の値を,それ以外の場合は第2引数の3倍の値を返す」lambda式を定義し,即座に第1引数=3,第2引数=7を入れて実行した場合,以下のようになる.

(lambda x, y: x*2 if x>y else y*3)(3, 7)

3は7より大きいのでelseの後ろのy*3が実行され,返される.

Pythonにおいてはlambda式単体ではあまり使い所がないのだが,mapやfilter等の関数と組み合わせることで効果を発揮する.

map関数

map関数は,第一引数にlambda式(もしくは関数オブジェクト),第2引数にイテレータ(range()関数等)やジェネレータ,listtupleを取る.第2引数で与えられたものの各要素に対して与えられたlambda式を適用した結果をイテレータとして返す.それをlist()関数に突っ込むことで,lambda式の操作が適用された新たなlistを手に入れることができる.

my_list = list(map(lambda x: x * 2, range(10)))

と書くと,変数my_listの中には,[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]というリストが入る.

元々range(10)[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]を順次出力していくイテレータを返すが,この各要素をlambda式の引数xで受け取り,lambda式の処理部分x * 2で2倍して,map関数はそのイテレータを返す,と行った具合である.

filter関数

前述のmapは各要素に対して処理をするlambda式をとるが,filter関数は,TureもしくはFalseを返す条件式のlambda式(もしくは関数オブジェクト)を取る.

my_list = list(filter(lambda x: x%2 == 0, range(10)))

lambda式がTrueを返す時の要素のみを集めたlistが生成される.つまりrange(10)の段階では[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]が帰ってくるが,filter関数の第一引数であるlambda x: x%2 == 0の部分が「その要素を2で割った時の剰余が0の時Trueを返す」,さらにfilter関数はそのlambda式がTrueの時のみ値を返すため,それがlistに入って[0, 2, 4, 6, 8]という偶数のみのlistが生成される.

内包表記

内包表記とは,与えられたリストに対するイテレータ処理をリスト内に書いてしまうことによりコード量を減らすというアクロバットな書法である.Python独自の書法で「Pythonicな書き方」の典型例と言われる.

同じようにしてジェネレータを定義することもできる(煩雑になるため本稿では省くが概念自体は覚えておくとよい).

ジェネレータとは,イテレータがあらかじめ作られたリストの中からnext関数(メッセージ)により一つずつ要素を取り出してくれるのに対して,一つずつ生成するものをいう.イテレータとジェネレータは同じように扱うことができる.range()関数はイテレータを返すため,値を一度に生成しているが,generatorその度ごとに返す値を生成する,という違いがある.

リスト内包表記

前述の「2倍にして返す」map関数の処理

list(map(lambda x: x * 2, range(10)))

をリスト内包表記で書くと,以下のようになる.

[x*2 for x in range(10)]

「ホントに何を言っているのかわからない」という声が多数あるが,最初の「x*2」がlambda式の部分にあたり,その引数xに対して値を与えているのが後半のfor文「for x in range(10)」である.

filter関数のようなlambdaに条件式を与える場合は,「さらにマジで何を言ってるのかわからない」状態の書法になる.

list(filter(lambda x: x%2 == 0, range(10)))

この処理を内包表記にすると,

[x for x in range(10) if x%2 == 0]

条件式の場合は,TrueもしくはFalseを返すlambda式が後ろに来ることに注意.

なぜ頭にもう一つxがあるかというと,lambda式がTrueを返した要素に対してさらに処理を書くことができるからである.

上記の式では当然[0, 2, 4, 6, 8]が返って来るが,さらに各要素を2倍したリストが欲しいとする.この場合,

[x*2 for x in range(10) if x%2 == 0]

とすると,[0, 4, 8, 12, 16]が返って来る,という次第である.

内包表記は速度が速い

なぜ内包表記を用いるかというと,単純に処理速度が早くなるからである.しかし数値計算の処理速度ではNumPyの関数群を使う方が早くなるし,可読性が下がることも無視できない.共同開発の時は他者が読めるかどうか確認してからこれらの内包表記を使う方がよい.