Python:为什么需要 functools.partial?
- 2025-01-20 09:06:00
- admin 原创
- 82
问题描述:
部分应用很酷。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
更有效率,或者更具可读性?
解决方案 1:
functools.partial
它提供了哪些无法通过 lambda 获得的功能?
就额外功能而言,没有太多改进(但稍后再看)——而且,可读性是每个人的眼光所及。
大多数熟悉函数式编程语言(尤其是 Lisp/Scheme 系列)的人似乎都很喜欢lambda
——我说“大多数”,绝对不是全部,因为 Guido 和我肯定是“熟悉”(等等)的人,但认为lambda
Python 中存在令人讨厌的异常现象……
他后悔曾经将它纳入 Python,而计划将其从 Python 3 中删除,因为这是“Python 的缺陷”之一。
我完全支持他。(我喜欢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]
现在将命名参数的可覆盖性加上三个属性的设置组合成一个表达式,然后告诉我它的可读性如何……!
解决方案 2:
嗯,这里有一个显示差异的例子:
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 中的“lambda 的局限性”和闭包:
Python 中的闭包(第 2 部分)
Python 中的闭包(第 3 部分)
解决方案 3:
在最新版本的 Python (>=2.7) 中,可以使用pickle
,partial
但不能使用lambda
:
>>> 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>
解决方案 4:
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的答案中讨论的早期和晚期绑定的效果。
解决方案 5:
除了 Alex 提到的额外功能之外,functools.partial 的另一个优势是速度。使用 partial,您可以避免构造(和破坏)另一个堆栈框架。
partial 和 lambdas 生成的函数默认都没有文档字符串(尽管您可以通过 为任何对象设置文档字符串__doc__
)。
您可以在此博客中找到更多详细信息:Python 中的偏函数应用
解决方案 6:
这是一个非常老的问题,但我想我会把它放在这里以防它对某人有用。
部分函数相对于 lambda 的一个优势在于它们在循环中的行为方式。由于 lambda 评估参数的方式,在使用它们时容易出错的方法是将 lambda 函数声明为循环的一部分(例如菜单项的回调方法)。
例如,这给了你一个答案,不熟悉 lambda 中这种行为的开发人员可能会感到惊讶,
funcs = []
for i in range(10):
f = lambda j: i + j
funcs.append(f)
print(f(1) for f in funcs)
# [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
然而,如果你使用部分,你就不会遇到这个问题;
parts = []
for i in range(10):
p = partial(sum, (i, ))
parts.append(p)
print([p(1) for p in parts])
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
当然,使用 lambda 可以解决这个问题,但是使用 partials 的语法(在我看来)更加简单并且行为符合你的预期。
解决方案 7:
我在第三个例子中最快地理解了意图。
当我解析 lambda 表达式时,我期望其复杂性/奇异性比标准库直接提供的要大。
另外,您会注意到第三个示例是唯一一个不依赖于完整签名的示例sum2
;从而使其耦合度稍微松散一些。
解决方案 8:
当评估某些变量时,泛函非常有用。
作为一个局外人,这里有一系列更友好的例子:
from functools import partial
sum = lambda x, y: x + y # sum(x, y) == x + y
n = 2
normalSum = lambda x: sum(x, n) # normalSum(x) == sum(x, y=n)
partialSum = partial(sum, y = n) # partialSum(sum(y=n)) == sum(x, 2)
print(normalSum(2), partialSum(2)) # 4 4
n = 6
print(normalSum(2), partialSum(2)) # 8 4
n
注意部分如何保存当时的值。
...
n = 2
partialSumOrig = partial(sum, y = n) # partialSumOrig(sum(y=n)) == sum(x, 2)
n = 6
partialSumNew = partial(sum, y = n) # partialSumNew(sum(y=n)) == sum(x, 6)
print(partialSumOrig(2), partialSumNew(2)) # 4 8
额外的示例展示了如何将参数传递到嵌套的 lambda 表达式中:
...
n = 8
partialSumOrig = partial(sum, y = n) # partialSumOrig(sum(y=n)) == sum(x, 8)
partialSumNew = partial(sum, n) # partialSumNew(sum(n)) == sum(8, y)
print(partialSumOrig(2)) # 10 # partialSumOrig(sum(2, 8)) == sum(2, 8)
print(partialSumNew(2)) # 10 # partialSumNew(sum(8, 2)) == sum(8, 2)
最后一个例子展示了如何在部分参数中传递:
...
n = 2
m = 2
partialSumSilly = partial(sum, n, m) # partialSumSilly(sum(n, m)) == sum(2, 2)
print(partialSumSilly()) # 4
最大的收获是:
normalSum()
行为类似于后期绑定,n
在运行时进行评估。partialSum()
行为类似于早期绑定,n
在定义时进行评估。
注意:实际上,由于 cpython 的解释特性,几乎所有内容都是后期绑定。