この2つは似てるけど違います。
- lambda式
- functools.partial
式かどうか、という単純な違いじゃありません、
もちろんそれも違うけどもっと根本的なことです。
そして両者の決定的な違いが分からず、
同じものと扱うと不都合なことが起きやすいです。
そこで Pythonでの lambda vs partial の違い
これを実例を元にまとめたいと思います。
このページの目次
lambda と partial は似ているものと思われがち
恐らく僕だけではないと思いますが……
このように考えてしまってました。
- lambdaで関数オブジェクトが作れる
- partialでも関数オブジェクトが作れる
- それならほぼ同じものでは……
もちろん別物ということは理解してます。
でも僕が思い違いしてたのは「使い方は違うけど関数オブジェクト作成に同じように使える」ということですね。理解が足りなさ過ぎたみたいです。
少なくとも僕は勘違いしてました。
lambda(ラムダ式)は無名関数を作るための構文
まずラムダ式(lambda)について
これは無名関数の作成のために使います。
▼ ラムダ式(lambda)についての詳細な解説
ラムダ式 (ラムダ形式とも呼ばれます) は無名関数を作成するのに使います。 式 lambda parameters: expression は関数オブジェクトになります。 この無名オブジェクトは以下に定義されている関数オブジェクト同様に動作します:
def <lambda>(parameters):
return expression
引数の一覧の構文は 関数定義 を参照してください。ラムダ式で作成された関数は文やアノテーションを含むことができない点に注意してください。引用元 : https://docs.python.org/ja/3/reference/expressions.html?highlight=lambda
▼ ラムダ式+関数オブジェクトの使用例
1 2 3 4 5 6 7 |
## ラムダ内で呼ばれる関数 def say(msg): print(msg) ## ラムダ定義&実行 msg = "Hello Pythoner!!" func = lambda: say(msg) func() |
このように名前のない関数 = 無名関数を作れる
関数オブジェクトとして実行できます。
そして重要なのは「関数オブジェクト同様に動作」という部分。これは関数定義をそのまま渡してるってことと同じです。
これがトラブルの温床(後述)にもなりえます。
partial()は新しい関数オブジェクトを返すメソッド
一方の functools.partial() の場合。
これは新規に関数オブジェクトを返してくれます。
より正確に書くならpartialオブジェクトを返します。
▼ 詳しいfunctools.partialの解説
functools.partial(func, /, *args, **keywords)¶
新しい partial オブジェクト を返します。このオブジェクトは呼び出されると位置引数 args とキーワード引数 keywords 付きで呼び出された func のように振る舞います。呼び出しに際してさらなる引数が渡された場合、それらは args に付け加えられます。引用元 : https://docs.python.org/ja/3/library/functools.html?highlight=partial#functools.partial
▼ partial()による関数オブジェクトの使用例
1 2 3 4 5 6 7 8 9 |
from functools import partial ## partial内で呼ばれる関数 def say(msg): print(msg) ## 関数オブジェクト作成&実行 msg = "Hello Pythoner!!" func = partial(say, msg) func() |
partialオブジェクトは呼び出し可能のこと
Python内部では関数オブジェクトとpartialオブジェクトは違います。
ただ動的に呼び出せるという点ではほぼ同じで差し支えないでしょう。
だからlamdabと同じと考えちゃうんですよね。
そこがコード的な落とし穴になりえます。
lambdaを関数オブジェクト作成に使う落とし穴
ここからは実際に僕がハマったポイントです。
関数オブジェクト作成にlambdaを使用してました。
▼ 簡略化した問題を引き起こしたコード例
1 2 3 4 5 6 7 8 9 10 11 12 |
def print_num(num): print("num : %d" % num) arr = [1,2,3,4,5] funcs = [] for i in arr: num = i f = lambda: print_num(num) funcs.append(f) for f in funcs: f() |
▼ このコードの実行結果……
1 2 3 4 5 |
num : 5 num : 5 num : 5 num : 5 num : 5 |
lambdaの性質を知らないから??となりました。
だって f = lambda: print_num(num) は関数オブジェクト代入として変ではないし、当然 1 ~ 5 までが出力されると期待したからです。
今でもこの原因をはっきりと説明できません。
恐らく f = lambda: ... によりスコープ内でラムダ式が再定義・上書きされ、それが最終的に関数オブジェクト配列の全要素に反映されたと結論づけました。
もし本当の原因が分かったら追記します。
partial()での関数オブジェクト作成の方が安全
そして以上の問題は partial() では起きません。
先ほどのコードをfunctools.partial()で書き直した結果
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from functools import partial def print_num(num): print("num : %d" % num) arr = [1,2,3,4,5] funcs = [] for i in arr: num = i f = partial(print_num, num) funcs.append(f) for f in funcs: f() |
▼ 期待する通りの出力結果に
1 2 3 4 5 |
num : 1 num : 2 num : 3 num : 4 num : 5 |
このようにpartialオブジェクトだと期待通りに。先ほど引用したように「新しい partial オブジェクト を返してくれる」というのが重要なポイントです。
少なくとも関数オブジェクトを動的生成する場合……
やはりfunctools.partial()を使うのが安全みたいです。
同一スコープでラムダは上書きされてしまう……?
なぜlambdaだと直観に反する結果が出てしまうのか。
先ほどの例だと 1 ~ 5 が順番に出力されず、
全て 5 が出力される意味不明な結果になります。
それは lambda が上書きされるからだと推測してます。
▼ もう1度問題を引き起こしたコードを再掲
1 2 3 4 5 6 7 8 9 10 11 12 |
def print_num(num): print("num : %d" % num) arr = [1,2,3,4,5] funcs = [] for i in arr: num = i f = lambda: print_num(num) funcs.append(f) for f in funcs: f() |
▼ このコードは次のスコープ内関数定義と同じ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def print_num(num): print("num : %d" % num) arr = [1,2,3,4,5] funcs = [] for i in arr: num = i def noname(): print_num(num) f = noname funcs.append(f) for f in funcs: f() |
▼ その結果 5 だけが出力される(?)
1 2 3 4 5 |
num : 5 num : 5 num : 5 num : 5 num : 5 |
恐らくこういう理由じゃないかと推測
ラムダ式(lambda)には名前がないですが、上コードのように def noname(): ... みたいにテンポラリな関数を作っているのと変わりません。
だから上書きされて最後だけ反映される、
そのように僕は考えています。
追記 : こういうご指摘コメントをいただいた
どうやら上の推論は間違っていたようです。
以下のようなコメントでご指摘いただきました。
f = lambda: print_num(num) がなぜうまくいかないかですが、
これは 変数 num がグローバル参照になっている為に、
最終的に num に代入されている値が参照されるからです。num = 10
for f in funcs: f()すると、
num : 10 が出力されます。引用元 : この記事へのコメント
このコメントで納得しました。
ラムダ式だけに注目していたため、グローバル変数が原因とは思っていませんでした。てっきりlambdaがこのような現象を起こしていると思い込んでたみたい
的確なご指摘ありがとうございます。
ちなみにJavaScriptの即自関数などの場合
少し気になったのが他言語の場合です。
例えばJavaScriptにもアロー関数ってあります。
これはPythonでのラムダ式と同じように使用可能
でもPythonのようなトラブルは発生しません。
▼ JS版で先ほどのpyコードを書き直した
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function print_num(num){ console.log("num : ", num) } const arr = [1,2,3,4,5] const funcs = [] for(let i=0; i<arr.length; i++){ const num = arr[i] const f = ()=>{ print_num(num) } funcs.push(f) } for(let i=0; i<funcs.length; i++){ funcs[i]() } |
▼ コンソール出力結果
1 2 3 4 5 |
num : 1 num : 2 num : 3 num : 4 num : 5 |
期待通りの結果になってる
JavaScriptでは関数はFunctionオブジェクトとして扱われ、アロー関数もPython lambdaみたいに上書きされることはないです。(毎回Functionオブジェクトが生成される)
恐らくPHPなどの言語でも同様かもしれない
とにかくPythonで無名関数を使う時、
「lambdaを使った関数オブジェクトの作成」
これには気を使った方がいいかもしれません。
Pythonでの予期せぬ動作・エラーなどの関連記事
こういったPython特有の挙動・エラーについて
次のような関連記事も書いたりしてます。
エラーが出るならまだ親切です。
問題なのは人間の期待通りに動いてくれない時
そういうケースには注意していきたいですね。ではまた