关于python:functools如何做它的功能?

How does functools partial do what it does?

我无法理解functools中的部分工作原理。
我从这里得到以下代码:

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

现在就行了

1
incr = lambda y : sum(1, y)

我得到的是,无论我传递给incr的论点,它都将作为y传递给lambda,它将返回sum(1, y),即1 + y

我明白那个。 但我不明白这个incr2(4)

4如何在部分函数中作为x传递? 对我来说,4应该替换sum2x4之间的关系是什么?


粗略地说,partial做了类似的事情(除了关键字args支持等):

1
2
3
4
5
6
7
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个参数的地方传递一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
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对象才能完成其工作:

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

所以,有几种解决方案:

自定义对象:

1
2
3
4
5
6
7
8
9
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))

LAMBDA:

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

有部分:

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

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


部分非常有用。

例如,在'pipe-lined'函数调用序列中(其中一个函数的返回值是传递给下一个函数的参数)。

有时这种管道中的函数需要一个参数,但紧接其上游的函数返回两个值。

在这种情况下,functools.partial可能允许您保持此功能管道的完整性。

这是一个特定的,孤立的示例:假设您希望按每个数据点与某个目标的距离对某些数据进行排序:

1
2
3
4
5
6
7
8
9
10
11
# 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)

要按目标距离对数据进行排序,您当然希望这样做:

1
data.sort(key=euclid_dist)

但你不能 - sort方法的key参数只接受带有一个参数的函数。

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

1
2
3
from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist现在接受一个参数,

1
2
>>> p_euclid_dist((3, 3))
  1.4142135623730951

所以现在你可以通过传入sort方法的key参数的partial函数来对数据进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

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

1
2
3
4
5
6
7
>>> from functools import partial

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

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

创建部分函数(使用关键字arg)

1
2
3
4
>>> pfnx = partial(fnx, a=12)

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

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

1
2
3
4
>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

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

1
2
3
4
5
6
7
>>> 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方法创建进程池:

1
2
3
4
>>> import multiprocessing as MP

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

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

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


部分可用于创建预先分配了一些输入参数的新派生函数

要查看部分实际使用情况,请参阅这篇非常好的博文:
http://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/

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

1
search(pattern, string, flags=0)

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

1
2
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方法签名中的第一个参数)。

这两个新函数(可调用)的签名是:

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

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

1
2
3
4
5
6
7
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()

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


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

1
2
3
4
5
6
7
8
9
10
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


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

1
2
3
4
5
6
7
8
9
10
11
12
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。


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

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

但是如果我们这样做,而是改变一个参数

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

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