关于函数编程:python:为什么需要functools.partial

Python: Why is functools.partial necessary?

部分应用程序很酷。functools.partial提供了哪些您无法通过lambdas的功能?

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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是更有效还是可读?


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

在额外的功能方面不多(但是,见下文),并且,可读性在旁观者眼中。大多数熟悉函数式编程语言的人(尤其是Lisp/Scheme家族的人)似乎都很喜欢lambda,我说"大多数",肯定不是全部,因为guido和我确实是那些"熟悉"(etc)的人之一,但我认为lambda在python中是一个令人眼花缭乱的异常现象…他后悔曾经接受它进入Python,而计划将它从Python3中删除,这是"Python的一个小故障"。我完全支持他。(我喜欢计划中的lambda……虽然它在Python中的局限性,以及它与其他语言不匹配的奇怪方式,使我的皮肤爬行)。

然而,对于一群爱以东的人来说,情况并非如此,他们策划了一场与Python历史上最接近的叛乱,直到圭多回去决定离开以东。虽然partial当然保留了(它不是完全复制,也不是一个眼痛),但没有发生(使函数返回常量、标识等)的几个可能的添加(避免显式复制lambda的更多功能)。

记住,lambda的身体被限制为一种表达,所以它有局限性。例如。。。:

1
2
3
4
5
6
7
8
9
>>> import functools
>>> f = functools.partial(int, base=2)
>>> f.args
()
>>> f.func
<type 'int'>
>>> f.keywords
{'base': 2}
>>>

functools.partial返回的函数被修饰为有助于自省的属性——它所包装的函数,以及它在其中修复的位置参数和命名参数。此外,命名参数可以立即被重写(在某种意义上,"修复"是默认设置):

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

所以,正如你所看到的,它显然没有lambda s: int(s, base=2)那么简单!-)

是的,你可以扭曲你的lambda来给你一些这个——例如,对于关键字重写,

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

但我非常希望,即使是最热心的lambda爱好者也不会认为这种恐怖比partial的呼吁更具可读性!-)由于python的lambda的"body's a single expression"限制(加上赋值永远不能成为python表达式的一部分),"attribute setting"部分更加困难。你最终会"在一个表达式中伪造任务",把列表理解扩展到远远超出其设计限制的程度……

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

现在,将命名参数的可重写性,加上三个属性的设置,组合成一个表达式,告诉我这将是多么的可读…!-)


好吧,这里有一个例子说明了不同之处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中的闭包:

  • python中的闭包(第2部分)
  • python中的闭包(第3部分)


在最新版本的python(>=2.7)中,可以使用picklea partial而不是lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> pickle.dumps(partial(int))
'cfunctools
partial
p0
(c__builtin__
int
p1
tp2
Rp3
(g1
(tNNtp4
b.'

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


Is functools somehow more efficient..?

作为对此的部分回答,我决定测试一下性能。下面是我的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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上,它给出了:

1
2
3
4
lambda creation time 3.1743163756961392
lambda execution time 3.040552701787919
partial creation time 3.514482823352731
partial execution time 1.7113973411608114

这意味着分部需要更多的时间来创建,但执行时间要少得多。这很可能是ARS答案中讨论的早期和后期绑定的效果。


除了上面提到的额外功能,functools.partial的另一个优点是速度。使用分部可以避免构造(和销毁)另一个堆栈帧。

默认情况下,分部和lambda生成的函数都没有doc string(尽管您可以通过__doc__为任何对象设置doc string)。

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


在第三个例子中,我最快地理解了意图。

当我解析lambda时,我期望比标准库直接提供的更复杂/更古怪。

另外,您会注意到,第三个示例是唯一一个不依赖于sum2的完整签名的示例,从而使其稍微松散地耦合。