Python:functools.partialが必要なのはなぜですか?


193

部分的なアプリケーションはクールです。functools.partialラムダを通過できない機能は何ですか?

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

functools、より効率的、あるいは読める何とか?

回答:


265

functools.partialラムダを通過できない機能は何ですか?

余分な機能に関してはそれほどではありませんが(ただし、後で参照)、読みやすさは見る人の目にあります。
関数型プログラミング言語(特にLisp / Schemeファミリの言語)に精通しているほとんどの人は問題ないと思われlambdaます。「ほとんど」と言いますが、すべてではありません。)しかしlambda、Pythonの目障りな異常と考えてください...
彼はこれをPythonに受け入れたことを悔い改め、「Pythonの不具合」の1つとしてPython 3から削除する予定でした。
その中で私は彼を完全にサポートしました。(私は大好きlambda スキームで ...しばらくその限界をPythonで、そして奇妙な方法、それはちょうどdoesnの 言語の残りの部分で、私の皮膚をクロールさせます)。

それほど、しかし、の大群のためのlambda愛好家-グイドは撤回して残すことを決めたまでは、これまでPythonの歴史の中で見反乱に最も近いものの一つを上演lambda。中に
いくつかの可能な添加物にfunctools(定数を返す関数を作成するには、アイデンティティ、など)は発生しませんでした(より多くlambdaのの機能が明示的に複製されることを回避するため)、partialもちろん残りました(完全な複製ではなく、目障りでもありません)。

ことを忘れないでくださいlambdaの体がに制限される表現、それは限界を持っているので、。例えば...:

>>> import functools
>>> f = functools.partial(int, base=2)
>>> f.args
()
>>> f.func
<type 'int'>
>>> f.keywords
{'base': 2}
>>> 

functools.partialの返される関数は、イントロスペクションに役立つ属性、つまり、ラップしている関数、およびその中で修正する位置引数と名前付き引数で装飾されています。さらに、名前付き引数はすぐに上書きできます(「修正」は、ある意味では、デフォルトの設定です)。

>>> f('23', base=10)
23

ご覧のとおり、それは明らかにlambda s: int(s, base=2)!-)ほど単純ではありません。

はい、ラムダをゆがめてこれをいくつか与えることができます。たとえば、キーワードの上書きの場合、

>>> f = lambda s, **k: int(s, **dict({'base': 2}, **k))

しかし、私は最も熱心な- 恋人でもこの恐怖を電話よりも読みやすいとは思わないことを心から望んでいます!-)。「属性の設定」の部分は、Pythonの「ボディは単一の式」の制限があるため(さらに、割り当てをPython式の一部にできないため)、結局は「式内の割り当てを偽る」ことになります。リストの理解をその設計の限界をはるかに超えて伸ばすことによって...:lambdapartiallambda

>>> f = [f for f in (lambda f: int(s, base=2),)
           if setattr(f, 'keywords', {'base': 2}) is None][0]

今、単一の式に、名前付き引数overridability、プラス3つの属性の設定を組み合わせて、そしてどれだけ読める教えてそれがあることを行っています...!


2
ええ、私はfunctools.partialあなたが言及した追加の機能性がラムダより優れていると思います。おそらくこれは別の投稿のトピックですが、設計レベルであなたがそれほど気になるのはlambda何ですか?
Nick Heiner、2010

11
@Rosarch、私が言ったように:最初に、それは制限です(Pythonは式とステートメントを明確に区別します- 単一の式内で実行できない、または賢明に実行できないことがたくさんあります、そしてそれがラムダの本体です); 第二に、そのまったく奇妙な構文糖。私は、Pythonの中に一つのことを、時間に戻って変更することができれば、それは不合理な、無意味な、目障りになるdeflambda、キーワード:それら両方作るfunction(Javascriptが得た1名の選択は本当に右)、そして私の異議の少なくとも3分の1が消えてしまいます!-)。言ったように、私はLispのラムダ異議はありません...!-)
Alex Martelli

1
@Alex Martelli、Guidoがラムダにこのような制限を設定したのはなぜですか:「ボディは単一の式」?C#のラムダ本体は、関数の本体で有効なものであれば何でもかまいません。Guidoがpython lambdaの制限を削除しないのはなぜですか?
ピーターロング

3
@PeterLongうまくいけば、Guidoがあなたの質問に答えることができます。その要点は、複雑すぎて、defとにかく使用できることです。私たちの慈悲深いリーダーが話しました!
new123456

5
@AlexMartelli Dropboxがグイドに興味深い影響を与えている- twitter.com/gvanrossum/status/391769557758521345
デヴィッド・

82

さて、ここに違いを示す例があります:

In [132]: sum = lambda x, y: x + y

In [133]: n = 5

In [134]: incr = lambda y: sum(n, y)

In [135]: incr2 = partial(sum, n)

In [136]: print incr(3), incr2(3)
8 8

In [137]: n = 9

In [138]: print incr(3), incr2(3)
12 8

Ivan Mooreによるこれらの投稿は、「ラムダの制限」とpythonのクロージャを拡張しています。


1
良い例え。私にとっては、これは実際にはラムダの「バグ」のように見えますが、他の人が意見を異にする可能性があることは理解しています。(いくつかのプログラミング言語で実装されているように、ループ内で定義されたクロージャーでも同様のことが起こります。)
ShreevatsaR 2010

28
この「アーリーバインディングとレイトバインディングのジレンマ」の修正は、必要に応じて、アーリーバインディングを明示的に使用することですlambda y, n=n: ...。後期(登場する名前の結合のみ機能の体内ではなく、その中defまたは同等のことはlambda)何ですが、私は過去に長いSO答えに長さで示してきたように、バグ:明示的に初期バインドそれはあなたが望むものだ際に、とき遅延バインディングのデフォルトを使用することは、あなたが望むものであり、それはだまさに Pythonのデザインの残りの文脈与えられた権利設計上の選択。
Alex Martelli、2010

1
@アレックス・マルテリ:ええ、ごめんなさい。関数を定義するときに実際に何かを定義していると思うので、適切に遅延バインディングに慣れず、予期しない驚きが頭痛の種になっています。(ただし、PythonよりもJavascriptで機能的なことをしようとすると、より多くなります。)多くの人遅延バインディングに慣れており、Pythonの他の設計と一貫していることを理解しています。私はまだあなたの他の長いSOの答えを読みたいです-リンク?:-)
ShreevatsaR 2010

3
アレックスは正しい、それはバグではありません。しかし、それは多くのラムダ愛好家を罠にかける「ごちゃごちゃ」です。haskel / functional型からの議論の「バグ」側については、Andrej Bauerの投稿を参照してください:math.andrej.com/2009/04/09/pythons-lambda-is-broken
ars

@ars:ああ、はい、Andrej Bauerの投稿へのリンクをありがとう。ええ、確かに遅延バインディングの影響は、数学タイプ(さらに悪いことに、Haskellの背景を持つ)が非常に予想外で衝撃的なものを見つけ続けているものです。:-)私がバウアー教授のところまで行き、それをデザインエラーと呼ぶかどうかはわかりませんが、人間のプログラマーが1つの考え方と別の考え方を完全に切り替えるの困難です。(または、おそらくこれは私の不十分なPythonエクスペリエンスにすぎ
ません

26

Pythonの最新バージョン(> = 2.7)では、することができます、しかし、ではありません。picklepartiallambda

>>> pickle.dumps(partial(int))
'cfunctools\npartial\np0\n(c__builtin__\nint\np1\ntp2\nRp3\n(g1\n(tNNtp4\nb.'
>>> pickle.dumps(lambda x: int(x))
Traceback (most recent call last):
  File "<ipython-input-11-e32d5a050739>", line 1, in <module>
    pickle.dumps(lambda x: int(x))
  File "/usr/lib/python2.7/pickle.py", line 1374, in dumps
    Pickler(file, protocol).dump(obj)
  File "/usr/lib/python2.7/pickle.py", line 224, in dump
    self.save(obj)
  File "/usr/lib/python2.7/pickle.py", line 286, in save
    f(self, obj) # Call unbound method with explicit self
  File "/usr/lib/python2.7/pickle.py", line 748, in save_global
    (obj, module, name))
PicklingError: Can't pickle <function <lambda> at 0x1729aa0>: it's not found as __main__.<lambda>

1
残念ながら、部分関数はのpickleに失敗しmultiprocessing.Pool.map()ます。stackoverflow.com/a/3637905/195139
2014年

3
@wtingその投稿は2010年のものpartialです。Python2.7でピクル可能です。
Fred Foo 2014年

22

functoolsはどういうわけかより効率的です。

これに対する部分的な回答として、私はパフォーマンスをテストすることにしました。これが私の例です:

from functools import partial
import time, math

def make_lambda():
    x = 1.3
    return lambda: math.sin(x)

def make_partial():
    x = 1.3
    return partial(math.sin, x)

Iter = 10**7

start = time.clock()
for i in range(0, Iter):
    l = make_lambda()
stop = time.clock()
print('lambda creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    l()
stop = time.clock()
print('lambda execution time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p = make_partial()
stop = time.clock()
print('partial creation time {}'.format(stop - start))

start = time.clock()
for i in range(0, Iter):
    p()
stop = time.clock()
print('partial execution time {}'.format(stop - start))

Python 3.3では次のようになります。

lambda creation time 3.1743163756961392
lambda execution time 3.040552701787919
partial creation time 3.514482823352731
partial execution time 1.7113973411608114

つまり、partialの作成には少し時間がかかりますが、実行にはかなり時間がかかります。これは、arsからの回答で説明されているアーリーバインディングとレイトバインディングの影響である可能性があります。


3
さらに重要なのpartialは、純粋なPythonではなくCで記述されていることです。つまり、単に別の関数を呼び出す関数を作成するよりも、より効率的なcallableを生成できます。
chepner 2017年

12

Alexが言及した追加の機能に加えて、functools.partialのもう1つの利点は速度です。パーシャルを使用すると、別のスタックフレームの構築(および破棄)を回避できます。

パーシャルによって生成された関数もラムダもデフォルトではdocstringを持ちません(ただし、を介して任意のオブジェクトのdocストリングを設定できます__doc__)。

詳細については、このブログをご覧ください:Pythonでの部分関数アプリケーション


速度の利点をテストした場合、ラムダを超える部分的な速度の向上はどの程度期待できますか?
Trilarion 2014

1
docstringが継承されると言うとき、どのPythonバージョンを参照しますか?Python 2.7.15およびPython 3.7.2では、継承されません。元のdocstringは、部分的に適用された引数を持つ関数に対して必ずしも正しいとは限らないため、これは良いことです。
1月

Python 2.7(docs.python.org/2/library/functools.html#partial-objects)の場合:「名前ドキュメントの属性は自動的に作成されません」。3. [5-7]についても同様です。
Yaroslav Nikitenko

リンクに誤りがあります:log_info = partial(log_template、level = "info")-レベルは例のキーワード引数ではないため、不可能です。Python 2と3の両方で、「TypeError:log_template()が引数 'level'の複数の値を取得しました」と述べています。
Yaroslav Nikitenko

実際、私は手作業でpartial(f)を作成し、docフィールドを「partial(func、* args、** keywords)-与えられた引数とキーワードを部分的に適用した新しい関数\ n」として提供しています。\ n '(両方Python 2および3の場合)。
Yaroslav Nikitenko

1

3番目の例では、意図が最も早くわかります。

ラムダを解析するとき、標準ライブラリが直接提供するよりも複雑さや奇妙さを期待しています。

また、3番目の例は、の完全な署名に依存しない唯一の例ですsum2。したがって、少し疎結合になります。


1
ええと、私は実際には反対の説得functools.partialです。ラムダは自明ですが、呼び出しを解析するのにずっと時間がかかりました。
デビッドZ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.