【问题标题】:Python: Why is functools.partial necessary?Python:为什么需要 functools.partial?
【发布时间】:2011-03-16 04:46:11
【问题描述】:

部分应用很酷。 functools.partial 提供了哪些您无法通过 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

functools 是否更高效或可读性更高?

【问题讨论】:

    标签: python functional-programming partial-application


    【解决方案1】:

    functools.partial 提供了哪些您无法通过 lambdas 获得的功能?

    没有太多额外的功能(但是,见后文)——而且,可读性是旁观者的眼中。
    大多数熟悉函数式编程语言的人(尤其是 Lisp/Scheme 家族的人)似乎都喜欢lambda,我说“大多数”,绝对不是全部,因为 Guido 和我肯定是那些“熟悉”(等)但认为lambda 是 Python 中令人眼花缭乱的异常......
    他后悔曾经将它接受到 Python 中,但计划将其从 Python 3 中删除,这是“Python 的故障”之一。
    我完全支持他。 (我喜欢lambda in 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-lover 也不会认为这种恐怖比partial 通话更具可读性!-)。 “属性设置”部分更加困难,因为 Python 的 lambda 的“主体是单个表达式”的限制(加上赋值永远不能成为 Python 表达式的一部分的事实)......你最终会“在内部伪造赋值”一个表达式”,通过将列表理解扩展到其设计限制之外......:

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

    现在将命名参数的可覆盖性以及三个属性的设置组合成一个表达式,并告诉我 的可读性如何......!

    【讨论】:

    • 是的,我想说您提到的functools.partial 的额外功能使其优于lambda。也许这是另一篇文章的主题,但在设计层面上,lambda 让您如此困扰的是什么?
    • @Rosarch,正如我所说:首先,它的局限性(Python 清晰地区分了表达式和语句——有很多事情你不能做,或者不能明智地做,在单个表达式中,这就是 lambda 的主体 );其次,它绝对奇怪的语法糖。如果我能回到过去并在 Python 中改变一件事,那将是荒谬、毫无意义、令人眼花缭乱的 deflambda 关键字:让它们都成为 function(一个名字选择 Javascript 得到了真的 i> 对),我的反对意见中至少有 1/3 会消失!-)。正如我所说,我不反对 lambda in Lisp...!-)
    • @Alex Martelli,Guido 为什么要为 lambda 设置这样的限制:“body's a single expression”? C# 的 lambda 主体可以是函数主体中的任何有效内容。为什么 Guido 不直接取消对 python lambda 的限制?
    • @PeterLong 希望Guido 可以回答您的问题。它的要点是它太复杂了,无论如何你都可以使用def。我们仁慈的领袖发话了!
    • @AlexMartelli DropBox 对 Guido 产生了有趣的影响 - twitter.com/gvanrossum/status/391769557758521345
    【解决方案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 的限制”和闭包:

    【讨论】:

    • 很好的例子。实际上,对我来说,这似乎更像是 lambda 的“错误”,但我理解其他人可能不同意。 (在循环中定义的闭包也会发生类似的情况,如在几种编程语言中实现的那样。)
    • 解决这个“早期与晚期绑定困境”的方法是在需要时明确使用早期绑定,lambda y, n=n: ...。后期绑定(名称出现在函数的主体中,而不是出现在其def 或等效的lambda 中)是任何的错误,正如我在过去的长 SO 答案中的长度:您在需要时显式地提前绑定,当 that 是您想要的时使用后期绑定默认值,那是 exactly考虑到 Python 设计的其余部分,正确的设计选择。
    • @Alex Martelli:是的,对不起。我只是不习惯正确地后期绑定,也许是因为我认为在定义函数时我实际上是在定义一些好的东西,而意想不到的惊喜只会让我头疼。 (不过,当我尝试在 Javascript 中做功能性事情时比在 Python 中更多。)我知道很多人对后期绑定感到满意,并且它与 Python 的其他设计是一致的。不过,我仍然想阅读您的其他长答案——链接? :-)
    • Alex 是对的,这不是错误。但这是一个让许多 lambda 爱好者陷入困境的“陷阱”。对于来自 haskel/function 类型的参数的“错误”方面,请参阅 Andrej Bauer 的帖子:math.andrej.com/2009/04/09/pythons-lambda-is-broken
    • @ars:是的,感谢 Andrej Bauer 帖子的链接。是的,后期绑定的影响肯定是我们数学类型(更糟糕的是,具有 Haskell 背景的人)不断发现的非常出乎意料和令人震惊的东西。 :-) 我不确定我是否会像 Bauer 教授那样称其为设计错误,但人类程序员很难在一种思维方式和另一种思维方式之间完全切换。 (或许这只是我的 Python 经验不足。)
    【解决方案3】:

    在最新版本的 Python (>=2.7) 中,您可以使用 picklepartial,但不能使用 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>
    

    【讨论】:

    • 不幸的是,部分函数无法为multiprocessing.Pool.map() 腌制。 stackoverflow.com/a/3637905/195139
    • @wting 那篇文章来自 2010 年。partial 在 Python 2.7 中是可腌制的。
    • 你可以用dill pickle lambdas
    【解决方案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
    

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

    【讨论】:

    • 更重要的是,partial 是用 C 语言编写的,而不是纯 Python,这意味着它可以产生比简单地创建调用另一个函数的函数更有效的调用。
    • 请注意,与显式传递参数相比,部分仍然有开销。见stackoverflow.com/questions/17388438/…
    【解决方案5】:

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

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

    您可以在此博客中找到更多详细信息:Partial Function Application in Python

    【讨论】:

    • 如果你已经测试过速度优势,partial over lambda 的速度提升是多少?
    • 当你说docstring是继承的,你指的是哪个Python版本?在 Python 2.7.15 和 Python 3.7.2 中,它们不会被继承。这是一件好事,因为原始文档字符串对于具有部分应用参数的函数不一定正确。
    • 对于 python 2.7 (docs.python.org/2/library/functools.html#partial-objects):“namedoc 属性不会自动创建”。与 3.[5-7] 相同。
    • 您的链接中有一个错误:log_info = partial(log_template, level="info") - 这是不可能的,因为 level 在示例中不是关键字参数。 python 2 和 3 都说:“TypeError: log_template() got multiple values for argument 'level'”。
    • 事实上,我手动创建了一个 partial(f) 并将 doc 字段作为 'partial(func, *args, **keywords) - 具有部分的新函数给定参数和关键字的应用程序\n。\n'(适用于 python 2 和 3)。
    【解决方案6】:

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

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

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

    【讨论】:

    • 嗯,其实我的看法正好相反,我花了很多时间来解析 functools.partial 调用,而 lambdas 是不言而喻的。
    猜你喜欢
    • 1970-01-01
    • 2011-02-13
    • 2018-02-15
    • 2019-06-09
    • 2017-11-18
    • 2017-03-16
    • 2017-05-31
    • 2023-02-05
    • 2014-08-09
    相关资源
    最近更新 更多