functools.partial 和类似的 lambda 表达式之间有什么区别?

30

在Python中,假设我有一个函数f,我想将其与一些附加参数一起传递(为简单起见,假设只有第一个参数保持变量)。

这两种方式做法有什么不同(如果有的话)?

# Assume secondary_args and secondary_kwargs have been defined

import functools

g1 = functools.partial(f, *secondary_args, **secondary_kwargs)
g2 = lambda x: f(x, *secondary_args, **secondary_kwargs)
partial文档页面中,有如下引用:

在类中定义的partial对象的行为类似于静态方法,在实例属性查找期间不会转换为绑定的方法。

如果将lambda函数用于从提供给类的参数(无论是在构造函数中还是稍后通过函数)创建类方法,那么它会受到这种影响吗?

1
我认为我的问题与链接的问题非常不同,但是那个问题的顶部答案非常详尽,它也恰好回答了我所寻找的大部分内容。如果它被关闭为重复问题,我不会抱怨。 - ely
我同意这不是重复问题,我已经重新开放了你的问题。供其他人参考,相关问题在这里 - wim
6个回答

17
  1. 一个 lambda 函数与标准函数具有相同的类型,因此它的行为类似于实例方法。

  2. 在您的示例中,partial 对象可以这样调用:

g1(x, y, z)

导致此调用(不是有效的 Python 语法,但你可以理解这个想法):

f(*secondary_args, x, y, z, **secondary_kwargs)

这个lambda只接受单个参数,并使用不同的参数顺序。(当然,这两个差异都可以克服 - 我只是回答你提供的两个版本之间的区别。)

  • partial对象的执行比等效的lambda执行略快。


  • 2
    你对比过 partial 对象和 lambda 的性能吗? - orlp
    5
    @nightcracker:是的,不是现在,但过去有几次。partial对象不创建Python代码对象,因此它保存了完整的Python堆栈帧。 - Sven Marnach
    @Sven Marnach:到底是哪个更快,创建代码、调用代码还是二者都是? - orlp
    5
    大多数情况下,只有执行时间很重要,所以这就是我测量的内容。 - Sven Marnach

    16

    摘要

    lambdafunctools.partial在常见用例中的实际区别似乎是:

    • functools.partial需要导入,而lambda则不需要。
    • 使用functools.partial创建的函数的函数定义可通过打印创建的函数来查看。使用lambda创建的函数应使用inspect.getsource()进行检查。

    对于lambdafunctools.partial,以下发现在实践中几乎相同:

    • 速度
    • Tracebacks

    速度(lambda vs functools.partial)

    我认为测试和真实数据比仅仅猜测哪一个更快更有说服力。

    看起来lambdafunctools.partial之间没有速度差异的统计证据。我运行了不同数量重复的不同测试,每次结果略有不同;任何一种方法都可能最快。速度是95%(2 sigma)置信度下相同的。这里是一些数值结果*

    # When functions are defined beforehand
    In [1]: timeit -n 1000 -r 1000 f_partial(data)
    23.6 µs ± 2.92 µs per loop (mean ± std. dev. of 1000 runs, 1000 loops each)
    
    In [2]: timeit -n 1000 -r 1000 f_lambda(data)
    22.6 µs ± 2.6 µs per loop (mean ± std. dev. of 1000 runs, 1000 loops each)
    
    # When function is defined each time again
    In [3]: timeit -n 1000 -r 1000 (lambda x: trim_mean(x, 0.1))(data)
    22.6 µs ± 1.98 µs per loop (mean ± std. dev. of 1000 runs, 1000 loops each)
    
    In [4]: timeit -n 1000 -r 1000 f_lambda = lambda x: trim_mean(x, 0.1); f_lambda(data)
    23.7 µs ± 3.89 µs per loop (mean ± std. dev. of 1000 runs, 1000 loops each)
    
    In [5]: timeit -n 1000 -r 1000 f_partial = partial(trim_mean, proportiontocut=0.1); f_partial(data)
    24 µs ± 3.38 µs per loop (mean ± std. dev. of 1000 runs, 1000 loops each)
    

    回溯

    我还尝试使用插入了字符串元素的列表运行f_lambdaf_partial,回溯(除了第一个条目)是相等的。所以在这方面没有区别。

    检查源代码

    • 通过打印创建的函数,可以看到使用functools.partial创建的函数定义。使用inspect.getsource()应检查由lambda创建的函数。
    # Can be inspected with just printing the function
    In [1]: f_partial
    Out[1]: functools.partial(<function trim_mean at 0x000001463262D0D0>, proportiontocut=0.1)
    
    In [2]: print(f_partial)
    functools.partial(<function trim_mean at 0x000001463262D0D0>, proportiontocut=0.1)
    
    # Lambda functions do not show the source directly
    In [3]: f_lambda
    Out[3]: <function __main__.<lambda>(x)>
    
    # But you can use inspect.getsource()
    In [4]: inspect.getsource(f_lambda)
    Out[4]: 'f_lambda = lambda x: trim_mean(x, 0.1)\n'
    
    # This throws a ValueError, though.
    In [5]: inspect.getsource(f_partial)
    

    附录

    * 测试中使用的设置

    from functools import partial
    from scipy.stats import trim_mean
    import numpy as np
    data = np.hstack((np.random.random(1000), np.random.random(50)*25000))
    
    f_lambda = lambda x: trim_mean(x, 0.1)
    f_partial = partial(trim_mean, proportiontocut=0.1)
    

    这些测试是在Python 3.7.3 64位版(Windows 10)上执行的。


    1
    “lambda”函数只接受一个位置参数,仅在定义时如此。它们可以被赋予任何签名(但没有注释)。 - Kyuuhachi

    9
    这里最重要的一点被忽略了 - lambda 与输入变量相关联,但是partition 在创建时会复制参数:
    >>> for k,v in {"1": "2", "3": "4"}.items():
    ...     funcs.append(lambda: print(f'{k}: {v}'))
    ...
    >>> print(funcs)
    [<function <lambda> at 0x106db71c0>, <function <lambda> at 0x10747a3b0>]
    >>> for f in funcs:
    ...     f()
    ...
    3: 4  # result are indentical
    3: 4
    

    >>> import functools
    >>> funcs = []
    >>> for k,v in {"1": "2", "3": "4"}.items():
    ...     funcs.append(functools.partial(print, f'{k}: {v}'))
    ...
    >>> print(funcs)
    [functools.partial(<built-in function print>, '1: 2'), functools.partial(<built-in function print>, '3: 4')]
    >>>
    >>> for f in funcs:
    ...     f()
    ...
    1: 2  # result differs
    3: 4
    

    1
    这并不是关于 lambdapartial 的比较。这只是在 partial 的情况下必须在调用时对参数进行求值(它返回一个函数),所以通过闭包作用域的解析发生在那个时间(就像任何函数调用一样)。lambda 是一个字面上的值(碰巧是一个函数),在定义时不会引发任何求值(以触发通过闭包作用域找到值)。 - ely
    2
    换句话说,您所展示的观点非常有价值,但它涉及到函数调用与值赋值引起的参数绑定问题。用“lambda”和“partial”演示只是一个例子,但任何其他通过值赋值与函数求值不同的替代方案都会显示相同的效果。 - ely
    4
    例如,考虑如果您为lambda情况制作了一个辅助函数 def mk_lambda(k, v): return lambda: print(f'{k}: {v}'),然后执行 funcs.append(mk_lambda(k, v))。函数调用 mk_lambda 的归纳将在调用时解析参数值,因此lambda稍后将在 funcs 中保留正确的参数。这不是lambda的属性,而是Python在何时解析作用域闭包的选择。 - ely
    2
    这里的解释可能从语言专家的角度来看不够完美,正如其他评论所指示的那样--- 但至少它对我来说立刻就“点亮了”。谢谢 @mrvol! - ev-br

    1

    局部函数不仅比等效的lambda表达式快约20%,而且它们保留与其相关函数的直接引用。而在lambda表达式中,该函数“被埋藏”在函数体内。

    => 如果您只需要解决推迟评估一个函数直到知道所有参数的问题,则使用partials。与将调用嵌入匿名函数(即lambda表达式)相比,您将拥有更好的内省方法。


    0
    我认为类方法只适用于在类定义过程中分配的函数。后面分配的函数并不特殊处理。
    除此之外,我个人更倾向于使用lambda函数,因为它们更常见,从而使代码更易于理解。
    class Foo(object):
        def __init__(self, base):
            self.int = lambda x:int(x, base)
    
    print Foo(4).int('11')
    

    6
    我个人更喜欢lambdas,因为它们更常见,从而使代码更易于理解。我以前从未听过这两个说法。 - phant0m
    @phant0m 我听到的关于Python lambda函数的大多数说法都带有“破碎”的字眼。;-) - Chris Wesseling
    @Chris functools.partial更加简洁,但我认为只要没有作用域问题,lambda更容易理解。首先,lambda意味着您不必记住functools.partial所需的精确参数顺序。 - Antimony

    0

    是的,lambda 会 "受苦"。而 partial 没有这个问题,因为它是一个带有调用运算符重载的对象,而不是一个真正的函数。

    但在类定义中使用 lambda 只是误用。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接