【问题标题】:How does functools partial do what it does?functools partial 是如何做到的?
【发布时间】:2013-02-26 05:23:30
【问题描述】:

我无法理解部分在 functools 中的工作原理。 我有来自here 的以下代码:

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

现在排队

incr = lambda y : sum(1, y)

我知道我传递给incr 的任何参数都将作为y 传递给lambda,这将返回sum(1, y),即1 + y

我明白这一点。但是我不明白这个incr2(4)

4 如何在偏函数中作为x 传递?对我来说,4 应该取代 sum2x4是什么关系?

【问题讨论】:

  • 简单的回答:没有!它实际上是作为y 传递的。尝试将print(f'x: {x}, y: {y}') 行添加到sum2 的顶部,您会看到,请在下面查看@MSK 的答案。

标签: python functional-programming partial-application functools


【解决方案1】:

大致上,partial 做了这样的事情(除了关键字参数支持等):

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*args)

    return wrapper

因此,通过调用partial(sum2, 4),您创建了一个新函数(准确地说是一个可调用函数),其行为类似于sum2,但少了一个位置参数。缺少的参数总是被4 代替,所以partial(sum2, 4)(2) == sum2(4, 2)

至于为什么需要它,有多种情况。仅举一个例子,假设您必须在预期有 2 个参数的地方传递一个函数:

class EventNotifier(object):
    def __init__(self):
        self._listeners = []

    def add_listener(self, callback):
        ''' callback should accept two positional arguments, event and params '''
        self._listeners.append(callback)
        # ...

    def notify(self, event, *params):
        for f in self._listeners:
            f(event, params)

但是您已经拥有的函数需要访问第三个context 对象才能完成其工作:

def log_event(context, event, params):
    context.log_event("Something happened %s, %s", event, params)

所以,有几种解决方案:

自定义对象:

class Listener(object):
   def __init__(self, context):
       self._context = context

   def __call__(self, event, params):
       self._context.log_event("Something happened %s, %s", event, params)


 notifier.add_listener(Listener(context))

拉姆达:

log_listener = lambda event, params: log_event(context, event, params)
notifier.add_listener(log_listener)

有部分:

context = get_context()  # whatever
notifier.add_listener(partial(log_event, context))

在这三个中,partial 是最短和最快的。 (对于更复杂的情况,您可能需要自定义对象)。

【讨论】:

  • 你从哪里得到extra_args变量
  • extra_args 是部分调用者传递的东西,在p = partial(func, 1); f(2, 3, 4) 的示例中是(2, 3, 4)
  • 但我们为什么要这样做,任何特殊用例,其中某些事情必须仅由部分完成而不能用其他事情完成
  • @user1865341 我在答案中添加了一个示例。
  • 用你的例子,callbackmy_callback是什么关系
【解决方案2】:

partials非常有用。

例如,在“流水线”函数调用序列中(其中一个函数的返回值是传递给下一个函数的参数)。

有时,此类管道中的函数需要单个参数,但紧接其上游的函数返回两个值

在这种情况下,functools.partial 可能允许您保持此函数管道完好无损。

这是一个具体的、孤立的示例:假设您想按每个数据点与某个目标的距离对某些数据进行排序:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

要按与目标的距离对这些数据进行排序,您当然想做的是:

data.sort(key=euclid_dist)

但是你不能——sort 方法的 key 参数只接受带有 single 参数的函数。

所以将euclid_dist重写为一个带有单个参数的函数:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist 现在接受一个参数,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

所以现在您可以通过传入排序方法的关键参数的偏函数来对数据进行排序:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

或者例如,函数的参数之一在外循环中发生变化,但在内循环中的迭代期间是固定的。通过使用部分,您不必在内部循环的迭代期间传递附加参数,因为修改后的(部分)函数不需要它。

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

创建一个偏函数(使用关键字 arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

您还可以使用位置参数创建偏函数

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

但这会抛出(例如,使用关键字参数创建部分然后使用位置参数调用)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

另一个用例:使用 python 的 multiprocessing 库编写分布式代码。使用 Pool 方法创建一个进程池:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool 有一个 map 方法,但它只需要一个可迭代的,所以如果需要传入参数列表较长的函数,请将函数重新定义为部分函数,​​以修复除一个之外的所有函数:

>>> ppool.map(pfnx, [4, 6, 7, 8])

【讨论】:

  • 这个功能有什么实际用途吗
  • @user1865341 在我的回答中添加了两个示例性用例
  • 恕我直言,这是一个更好的答案,因为它排除了对象和类等不相关的概念,并专注于这就是全部的功能。
【解决方案3】:

Partials 可用于生成新的派生函数,这些函数预先分配了一些输入参数

要查看部分现实世界的一些用法,请参阅这篇非常好的博文here

博客中的一个简单而简洁的初学者示例,介绍了如何在re.search 上使用partial 以使代码更具可读性。 re.search 方法的签名是:

search(pattern, string, flags=0) 

通过应用partial,我们可以创建多个版本的正则表达式search来满足我们的要求,例如:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

现在is_spaced_apartis_grouped_together 是从re.search 派生的两个新函数,它们应用了pattern 参数(因为patternre.search 方法签名中的第一个参数)。

这两个新函数(callable)的签名是:

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

这就是您可以在某些文本上使用这些部分函数的方式:

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

您可以参考上面的link 以更深入地了解该主题,因为它涵盖了这个具体示例等等。

【讨论】:

  • 这不等于is_spaced_apart = re.compile('[a-zA-Z]\s\=').search吗?如果是这样,是否可以保证partial 成语编译正则表达式以便更快地重用?
【解决方案4】:

简短的回答,partial 为函数的参数提供默认值,否则该函数将没有默认值。

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10

【讨论】:

  • 这是对的一半,因为我们可以覆盖默认值,我们甚至可以通过后续的partial 等覆盖覆盖的参数
【解决方案5】:

在我看来,这是在python中实现currying的一种方式。

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

结果是 3 和 4。

【讨论】:

  • 嗯,不完全是。柯里化是将具有 n 个参数的函数划分为具有一个参数的 n 个连续函数。部分应用是用一些参数“预填充”一个函数,然后返回一个参数较少的函数
【解决方案6】:

另外值得一提的是,当部分函数传递另一个我们想要“硬编码”一些参数的函数时,那应该是最右边的参数

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

但如果我们做同样的事情,只是改变一个参数

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

它会抛出错误, "TypeError: func() 得到了参数 'a' 的多个值"

【讨论】:

  • 嗯?你这样做最左边的参数是这样的:prt=partial(func, 7)
【解决方案7】:

这个答案更像是一个示例代码。以上所有答案都很好地解释了为什么应该使用部分。我将给出关于部分的观察和用例。

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

以上代码的输出应该是:

a:1,b:2,c:3
6

请注意,在上面的示例中,返回了一个新的可调用对象,它将参数 (c) 作为其参数。请注意,它也是函数的最后一个参数。

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

上述代码的输出也是:

a:1,b:2,c:3
6

请注意, * 用于解包非关键字参数,根据它可以采用的参数返回的可调用对象与上述相同。

另一个观察结果是: 下面的示例演示了 partial 返回一个可调用的,它将采用 未声明的参数 (a) 作为参数。

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

以上代码的输出应该是:

a:20,b:10,c:2,d:3,e:4
39

同样,

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

上面的代码打印

a:20,b:10,c:2,d:3,e:4
39

当我使用来自multiprocessing 模块的Pool.map_async 方法时,我不得不使用它。您只能将一个参数传递给工作函数,因此我不得不使用partial 使我的工作函数看起来像一个只有一个输入参数的可调用函数,但实际上我的工作函数有多个输入参数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-24
    • 1970-01-01
    • 2018-09-27
    • 2011-06-25
    • 1970-01-01
    • 1970-01-01
    • 2016-12-20
    相关资源
    最近更新 更多