Python lambda vs partial の決定的違い。似てるけど別物だった

この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

▼ ラムダ式+関数オブジェクトの使用例

このように名前のない関数 = 無名関数を作れる

関数オブジェクトとして実行できます。

そして重要なのは「関数オブジェクト同様に動作」という部分。これは関数定義をそのまま渡してるってことと同じです。

これがトラブルの温床(後述)にもなりえます。

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()による関数オブジェクトの使用例

partialオブジェクトは呼び出し可能のこと

Python内部では関数オブジェクトとpartialオブジェクトは違います。
ただ動的に呼び出せるという点ではほぼ同じで差し支えないでしょう。

だからlamdabと同じと考えちゃうんですよね。

そこがコード的な落とし穴になりえます。

lambdaを関数オブジェクト作成に使う落とし穴

ここからは実際に僕がハマったポイントです。

関数オブジェクト作成にlambdaを使用してました。

▼ 簡略化した問題を引き起こしたコード例

▼ このコードの実行結果……

lambdaの性質を知らないから??となりました。

だって f = lambda: print_num(num)  は関数オブジェクト代入として変ではないし、当然 1 ~ 5 までが出力されると期待したからです。

今でもこの原因をはっきりと説明できません。

恐らく f = lambda: ...  によりスコープ内でラムダ式が再定義・上書きされ、それが最終的に関数オブジェクト配列の全要素に反映されたと結論づけました。

もし本当の原因が分かったら追記します。

partial()での関数オブジェクト作成の方が安全

そして以上の問題は partial() では起きません。

先ほどのコードをfunctools.partial()で書き直した結果

▼ 期待する通りの出力結果に

このようにpartialオブジェクトだと期待通りに。先ほど引用したように「新しい partial オブジェクト を返してくれる」というのが重要なポイントです。

少なくとも関数オブジェクトを動的生成する場合……

やはりfunctools.partial()を使うのが安全みたいです。

同一スコープでラムダは上書きされてしまう……?

なぜlambdaだと直観に反する結果が出てしまうのか。

先ほどの例だと 1 ~ 5 が順番に出力されず、
全て 5 が出力される意味不明な結果になります。

それは lambda が上書きされるからだと推測してます。

▼ もう1度問題を引き起こしたコードを再掲

▼ このコードは次のスコープ内関数定義と同じ

▼ その結果 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コードを書き直した

▼ コンソール出力結果

期待通りの結果になってる

JavaScriptでは関数はFunctionオブジェクトとして扱われ、アロー関数もPython lambdaみたいに上書きされることはないです。(毎回Functionオブジェクトが生成される)

恐らくPHPなどの言語でも同様かもしれない

とにかくPythonで無名関数を使う時、

「lambdaを使った関数オブジェクトの作成」

これには気を使った方がいいかもしれません。

Pythonでの予期せぬ動作・エラーなどの関連記事

こういったPython特有の挙動・エラーについて

次のような関連記事も書いたりしてます。

エラーが出るならまだ親切です。

問題なのは人間の期待通りに動いてくれない時

そういうケースには注意していきたいですね。ではまた

Shareこの記事をシェアしよう!

Commentsこの記事についたコメント

2件のコメント
  • マサ

    通りがかりのエンジニアです。

    f = lambda: print_num(num) がなぜうまくいかないかですが、

    これは 変数 num がグローバル参照になっている為に、
    最終的に num に代入されている値が参照されるからです。

    num = 10
    for f in funcs: f()

    すると、
    num : 10 が出力されます。

    以上参考までに。

    11月 22, 2022 11:07 am
    • ぴー助

      大変勉強になるコメントありがとうございます。

      僕はlambdaばかりに注目していたので、numがグローバル変数であることを忘れていました。まさしくその通りだと考えたため、記事を修正したいと思います。

      コメントありがとうございました。

      11月 22, 2022 3:31 pm

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください