functools partial 如何完成其功能?

2025-02-20 09:23:00
admin
原创
30
摘要:问题描述:我无法理解它的partial工作原理。我从这里functools有以下代码:>>> sum = lambda x, y : x + y >>> sum(1, 2) 3 >>> incr = lambda y : sum(1, y) >>...

问题描述:

我无法理解它的partial工作原理。我从这里functools有以下代码:

>>> 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被传递ylambda返回sum(1, y)1 + y

我理解这一点。但我不明白这一点incr2(4)

在部分函数中如何4传递?对我来说,应该替换。和之间有什么关系?x`4sum2x`4


解决方案 1:

大致来说,partial执行以下操作(除了关键字参数支持等):

def partial(func, *part_args):
    def wrapper(*extra_args):
        return func(*part_args, *extra_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))

Lambda:

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它是最短和最快的。(不过,对于更复杂的情况,您可能需要自定义对象)。

解决方案 2:

部分非常有用。

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

有时,此类管道中的某个函数只需要一个参数,但其上游的紧邻函数却返回两个值

在这种情况下,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参数只接受带有单个参数的函数。

因此将其重写euclid_dist为一个接受一个参数的函数:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist现在接受一个参数,

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

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

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

解决方案 3:

简而言之,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

解决方案 4:

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

要了解部分内容的实际用法,请参阅此处的这篇非常好的博客文章

博客中有一个简单但简洁的初学者示例,介绍了如何使用partialonre.search使代码更具可读性。 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应用了参数(因为pattern是方法签名中的第一个参数re.search)。

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

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

您可以参考上面的链接来更深入地了解该主题,因为它涵盖了这个特定的例子以及更多内容。

解决方案 5:

在我看来,这是在 Python 中实现柯里化的一种方法。

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。

解决方案 6:

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

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

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

解决方案 7:

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

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’获得了多个值”

解决方案 8:

添加机器学习中的几个案例,其中函数式编程柯里functools.partial可能非常有用:

在同一数据集上构建多个模型

下面的示例展示了如何在同一数据集上拟合linear regressionsupport vector machine和回归模型,以预测目标并计算分数。random forest`diabetes`

(部分) 函数是通过柯里化 (使用)classify_diabetes()从函数创建的。后面的函数不再需要传递数据,我们可以直接传递模型类的实例。classify_data()`functools.partial()`

from functools import partial
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_diabetes

def classify_data(data, model):
    reg = model.fit(data['data'], data['target'])
    return model.score(data['data'], data['target'])

diabetes = load_diabetes()
classify_diabetes = partial(classify_data, diabetes) # curry
for model in [LinearRegression(), SVR(), RandomForestRegressor()]:
    print(f'model {type(model).__name__}: score = {classify_diabetes(model)}')

# model LinearRegression: score = 0.5177494254132934
# model SVR: score = 0.2071794500005485
# model RandomForestRegressor: score = 0.9216794155402649

设置机器学习管道

pipeline()这里使用柯里化创建函数,该函数StandardScaler()在对数据进行模型拟合之前已经用于预处理(缩放/规范化),如下一个示例所示:

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

pipeline = partial(make_pipeline, StandardScaler()) # curry    
for model in [LinearRegression(), SVR(), RandomForestRegressor()]:
    print(f"model {type(model).__name__}: " \n          f"score = {pipeline(model).fit(diabetes['data'], diabetes['target'])\n                                 .score(diabetes['data'], diabetes['target'])}")

# model LinearRegression: score = 0.5177494254132934
# model SVR: score = 0.2071794500005446
# model RandomForestRegressor: score = 0.9180227193805106

解决方案 9:

对于那些想知道该partial函数如何工作的人,请考虑该函数的实现my_partial,它具有与该函数相同的功能functools.partial

my_partial 函数

def my_partial(func, *_args, **_kwargs):
    def wrapper(*args, **kwargs):
        return func(*(_args + args), **dict(_kwargs, **kwargs))
    return wrapper

my_partial函数func*_args(列表参数) 和**_kwargs(关键字参数) 作为参数。使用_args_kwargs,我们可以将初始参数my_partial与函数一起固定给函数。

在函数内部my_partial,我们有另一个函数。当使用带有一些可选参数的函数作为参数调用wrapper外部函数( )时,我们返回该函数,该函数也接受列表参数和关键字参数。my_partial`wrapper`

由于my_partialwrapper函数都以这种方式接受参数,我们可以将参数传递给函数两次。首先,我们调用my_partial并传递一个带有可选参数的函数,然后该my_partial函数返回wrapper function也接受选项参数的函数。这样我们就可以传递两次参数。

函数wrapper将传递给 的位置参数my_partial与其参数组合起来。函数调用作为参数传递给 的wrapper函数 ( ) 。func`my_partial`

在包装器中,我们返回func(*(_args + args), **dict(_kwargs, **kwargs))。这里,_argsargs都是tuples,并且+是连接运算符,它将和_args中的元素组合在一起。注意我们之前args提到过,因为有传递给的参数,并且由于它是第一个传递的,所以我们必须先得到它。这里,将两个字典合并为一个字典。_args`args_argsmy_partial`dict(**_kwargs, **kwargs)

用例:

通常,一个函数可能需要很多参数,并且假设我们必须重复使用该函数。在这种情况下,我们可以用 包装该函数partial,指定可能需要多次传递给该函数的固定参数。

from functools import partial

def multiply(a, b):
    return a * b

double = partial(multiply, 2)

print(double(10))

输出:

20

请注意,在上面的语句中partial(multiply, 2),我们指定参数 a 为multiply2。因此,当我们必须将一个值加倍时,我们不需要将值 2 多次传递给乘法函数。

这确实是一个简单的函数(multiply),但通常情况下,我们必须向函数传递许多参数。在这种情况下,我们可以使用 partial 来指定可能需要多次传递的参数。

解决方案 10:

仅添加一个用例。

functools.partial在多处理中非常有用。例如,当您想要使用一个接受三个ints以及两个常量s的函数时:list`int`int

from multiprocessing import Pool, cpu_count
from functools import partial

def run(a, b, c):
    return a+b+c

a = [1,2,3,4,5]
b = 10
c = 20

with Pool(cpu_count()) as pool:
    results = pool.map(partial(run, b=b, c=c), a)
print(results)

出去:

[31,32,33,34,35]

等效于不multiprocessing使用、使用functools.partialfor 循环,例如:

from functools import partial

def run(a, b, c):
    return a+b+c

a = [1,2,3,4,5]
b = 10
c = 20

p = partial(run, b=b, c=c)
results = []

for i in a:
    results.append(p(i))

print(results)

没有functools.partial或的等价物multiprocessing,例如:

results = []
for i in a:
    results.append(run(i, b, c))
print(results)

这样做的原因是multiprocessing.Pool.pool.map需要,因此可以args = (func, iterable(object))在需要多个参数func的地方使用,并且在您的用例中第一个参数会变化,其余参数为常量。注意:使用 时按 的顺序;使用可能更快,但顺序未知。object`partialfuncresultsapool.mappool.map_asyncresults`

len(a)很大时,该multiprocessing示例比使用 for 循环的示例性能更好,因为它pool.map(partial(run, b, c), a)使用cpu_count()进程并行执行。

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1325  
  IPD(Integrated Product Development)流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。它涵盖了从产品概念产生到产品退市的整个生命周期,通过整合跨部门团队、优化流程等方式,显著提升产品开发的效率和质量,进而为项目的成功奠定坚实基础。深入探究IPD流程的五个阶段与项目成功之间...
IPD流程分为几个阶段   4  
  华为作为全球知名的科技企业,其成功背后的管理体系备受关注。IPD(集成产品开发)流程作为华为核心的产品开发管理模式,其中的创新管理与实践更是蕴含着丰富的经验和深刻的智慧,对众多企业具有重要的借鉴意义。IPD流程的核心架构IPD流程旨在打破部门墙,实现跨部门的高效协作,将产品开发视为一个整体的流程。它涵盖了从市场需求分析...
华为IPD是什么   3  
  IPD(Integrated Product Development)研发管理体系作为一种先进的产品开发模式,在众多企业的发展历程中发挥了至关重要的作用。它不仅仅是一套流程,更是一种理念,一种能够全方位提升企业竞争力,推动企业持续发展的有效工具。深入探究IPD研发管理体系如何助力企业持续发展,对于众多渴望在市场中立足并...
IPD管理流程   3  
  IPD(Integrated Product Development)流程管理旨在通过整合产品开发流程、团队和资源,实现产品的快速、高质量交付。在这一过程中,有效降低成本是企业提升竞争力的关键。通过优化IPD流程管理中的各个环节,可以在不牺牲产品质量和性能的前提下,实现成本的显著降低,为企业创造更大的价值。优化产品规划...
IPD流程分为几个阶段   4  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用