Python:为什么functools.partial是必需的?

问题:Python:为什么functools.partial是必需的?

部分应用程序很酷。哪些功能functools.partial提供了lambda无法获得的功能?

>>> 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某种方式更有效或更可读吗?

Partial application is cool. What functionality does functools.partial offer that you can’t get through lambdas?

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

Is functools somehow more efficient, or readable?


回答 0

哪些功能functools.partial提供了lambda无法获得的功能?

在额外功能方面并没有太多(但是,请参阅稍后)–旁观者眼中的可读性。
大多数熟悉函数式编程语言的人(尤其是Lisp / Scheme系列的人)看起来都lambda很好–我说“大多数”,绝对不是全部,因为Guido和我肯定是“熟悉”的人(等) 却被认为是lambdaPython中的一种令人眼花an乱的异常……
他为曾经接受过Python而打算将其从Python 3中删除(作为“ Python的小故障”之一)感到re悔。
我对此表示完全支持。(我喜欢lambda Scheme,但是它在Python中有局限性,而且它只是奇怪的方式而没有 与其他语言一起使用,让我的皮肤爬行)。

但是,对于成群的lambda恋人而言并非如此-他们在Python的历史中上演过最接近叛逆的事情之一,直到Guido回溯并决定离开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)!-)那样简单

是的,您可以扭曲您的lambda来为您提供一些帮助-例如,对于关键字覆盖,

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

但我非常希望,即使是最热心的- lambda情人,也不要认为这种恐怖比partial通话更容易理解!-)。由于Python的“主体是单个表达式”的局限性,“属性设置”部分更加困难lambda(加上赋值永远不能成为Python表达式的一部分这一事实)……您最终会“伪造表达式中的赋值”通过将列表理解范围扩展到远远超出其设计限制…:

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

现在结合命名参数覆盖性,再加上三个属性的设置,到一个单一的表达,并告诉我是多么可读将是…!

What functionality does functools.partial offer that you can’t get through lambdas?

Not much in terms of extra functionality (but, see later) – and, readability is in the eye of the beholder.
Most people who are familiar with functional programming languages (those in the Lisp/Scheme families in particular) appear to like lambda just fine – I say “most”, definitely not all, because Guido and I assuredly are among those “familiar with” (etc) yet think of lambda as an eyesore anomaly in Python…
He was repentant of ever having accepted it into Python whereas planned to remove it from Python 3, as one of “Python’s glitches”.
I fully supported him in that. (I love lambda in Scheme… while its limitations in Python, and the weird way it just doesn’t fit in with the rest of the language, make my skin crawl).

Not so, however, for the hordes of lambda lovers — who staged one of the closest things to a rebellion ever seen in Python’s history, until Guido backtracked and decided to leave lambda in.
Several possible additions to functools (to make functions returning constants, identity, etc) didn’t happen (to avoid explicitly duplicating more of lambda‘s functionality), though partial did of course remain (it’s no total duplication, nor is it an eyesore).

Remember that lambda‘s body is limited to be an expression, so it’s got limitations. For example…:

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

functools.partial‘s returned function is decorated with attributes useful for introspection — the function it’s wrapping, and what positional and named arguments it fixes therein. Further, the named arguments can be overridden right back (the “fixing” is rather, in a sense, the setting of defaults):

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

So, as you see, it’s definely not as simplistic as lambda s: int(s, base=2)!-)

Yes, you could contort your lambda to give you some of this – e.g., for the keyword-overriding,

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

but I dearly hope that even the most ardent lambda-lover doesn’t consider this horror more readable than the partial call!-). The “attribute setting” part is even harder, because of the “body’s a single expression” limitation of Python’s lambda (plus the fact that assignment can never be part of a Python expression)… you end up “faking assignments within an expression” by stretching list comprehension well beyond its design limits…:

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

Now combine the named-arguments overridability, plus the setting of three attributes, into a single expression, and tell me just how readable that is going to be…!


回答 1

好吧,这是一个显示差异的示例:

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的这些帖子扩展了“ lambda的局限性”和python中的闭包:

Well, here’s an example that shows a difference:

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

These posts by Ivan Moore expand on the “limitations of lambda” and closures in python:


回答 2

在最新版本的Python(> = 2.7)中,您可以pickle使用partial,但不能使用lambda

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

In the latest versions of Python (>=2.7), you can pickle a partial, but not a lambda:

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

回答 3

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的答案中讨论的早期绑定和后期绑定的效果。

Is functools somehow more efficient..?

As a partly answer to this I decided to test the performance. Here is my example:

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))

on Python 3.3 it gives:

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

Which means that partial needs a bit more time for creation but considerably less time for execution. This can well be the effect of the early and late binding which are discussed in the answer from ars.


回答 4

除了Alex提到的额外功能之外,functools.partial的另一个优点是速度。使用partial,您可以避免构造(和破坏)另一个堆栈框架。

默认情况下,由partial和lambda生成的函数都没有文档字符串(尽管您可以通过以下方式为任何对象设置文档字符串 __doc__)。

您可以在此博客中找到更多详细信息:Python中的部分函数应用程序

Besides the extra functionality Alex mentioned, another advantage of functools.partial is speed. With partial you can avoid constructing (and destructing) another stack frame.

Neither the function generated by partial nor lambdas have docstrings by default (though you can set the doc string for any objects via __doc__).

You can find more details in this blog: Partial Function Application in Python


回答 5

我在第三个示例中最快地了解了意图。

当我解析lambda时,我期望比直接由标准库提供的复杂性/奇数更高。

另外,您会注意到,第三个示例是唯一一个不依赖于sum2; 的完整签名的示例。因此使其耦合松散一些。

I understand the intent quickest in the third example.

When I parse lambdas, I’m expecting more complexity/oddity than offered by the standard library directly.

Also, you’ll notice that the third example is the only one which doesn’t depend on the full signature of sum2; thus making it slightly more loosely coupled.