其他分享
首页 > 其他分享> > 偏函数partial

偏函数partial

作者:互联网

前言

引入例子

from functools import partial

def demo(x, y, z):
    print(x,y,z)

new_demo = partial(demo,1)
new_demo(2,3)

输出:1 2 3

直观感受: 就是返回一个已经固定了部分参数和原函数功能一样的函数

再次举例:

new_demo = partial(demo,x=2)
new_demo(2,3)  #TypeError: demo() got multiple values for argument 'x' 报错,重复参数 x 

思考: 可能是因为已经对x定义为关键字参数,所以后续的y和z也必须为关键字参数【位置参数必须在关键字参数之前】,相当于demo(x=1,2,3)但是报错应该是:SyntaxError:positional argument follows keyword argument 测试如下:

def test(a,b):
    pass
# 关键字参数在位置参数前面,报错如下
test(a=1,2)

报错:SyntaxError: positional argument follows keyword argument

如果是报错重复参数的话,应该是如下这种情况:

def test(a,b):
    pass

test(1,a=2)

报错:TypeError: test() got multiple values for argument 'a'

所以偏函数的机制没有那么简单,看看下述源码分析


分析

class partial:
    """New function with partial application of the given arguments
    and keywords.
    """

    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(partial, cls).__new__(cls)

        self.func = func
        self.args = args
        self.keywords = keywords
        return self

    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

构造方法分析

形参中的/

/ 前的参数,只能以位置参数来传递,不能以关键字形式传递

def test(a,b,/,c):
    pass

test(1,2,c=3)
test(1,b=2,c=3) # 报错: TypeError: test() got some positional-only arguments passed as keyword arguments: 'b'

所以只能通过位置参数传递需要“偏函数处理的func”给构造方法,如果是通过键值对形式传递func参数会报错如下:

def func(a,b):
    pass

partial(func=func,a=1)
#TypeError: type 'partial' takes at least one argument
#因为这里的func=func,a=1 会被当做构造方法的 **kwargs参数接受

partial(func,a=1) #这么写就不会报错,必须通过位置参数来传递

第一个if

第一个判断:如果传递进去的func不是callable的,报错,举例如下:

from functools import partial

demo = 1

partial(demo) #TypeError: the first argument must be callable

所以,能partial的不仅仅是函数,只要是能调用的 都是能进行处理的,比如一个类,实现了 __call__方法

第二个if

if hasattr(func, "func"):这里是对已经partial处理过一次的对象,再次进行partial处理时所做的逻辑,在嵌套进行偏函数处理,即:partial的func参数接收的是一个partial实例的时候,把两次显示声明固定的参数组合起来

后续逻辑

self = super(partial, cls).__new__(cls)

self.func = func
self.args = args
self.keywords = keywords
return self

构造方法 返回partial实例,赋予 实例属性 func/args/keywords,测试如下:

from functools import partial

def func(a,b,c):
    pass

obj = partial(func,1,b=2)
print(obj)#functools.partial(<function func at 0x000001F1A84DEF70>, 1, b=2)
print(obj.func)#<function func at 0x000001F1A84DEF70>
print(obj.args)#(1,)
print(obj.keywords){'b': 2}

此时输出obj.__dict__是一个空字典,因为partial定义了__slots__

call方法

    def __call__(self, /, *args, **keywords):
        # 去重,所以partial 对象调用的时候 ()参数能传递 声明时候相同的键值对 但是不能多传位置参数
        ##  关键字参数 ,重新解包打包的字典,call调用的关键字参数放在后面,所以能覆盖之前字典中 同名的key的value
        ##  但是前面的self.args 这种就不能重新传入了,会报错
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

关键字参数keywords: self.keywords是诸如实例化时res = partial(func,a=1,b=2) 对应的 {'a':1,'b':2},如果 实例化返回的partial对象res调用的时候再次传入键值对参数res(b=3),这时候__call__中的第一行相当于 keywords = {**{'a':1,'b':2},**{'b':3}}, 双星号 解包之后,相当于最后keywords = {'a':1,'b':3} ,所以在res调用的时候是可以重新传递前面固定的关键字参数,但是不能重新传递前面固定的位置参数,因为return self.func(*self.args, *args, **keywords)这里已经把之前固定的self.args放置好,回到一开始前言中的问题,这就是为什么会报错参数重复了

举例

partial实例调用时,可以重新传递已经固定好的关键字参数,但是重新传递时,只能按照关键字参数传递

def add(x,y):
    return x+y

new_add = partial(add,y=2)
print(new_add(1))
# print(new_add(1,2)) # 会报错,重复了y,看call中的**keywords已经为y=2了,这时候args 还传递了两个参数,所以重复了
print(new_add(1,y=2))# 重新传递正常执行

调用时,重新传递位置参数会报错

def add(x,y):
    return x+y

new_add = partial(add,1) # 传递位置参数给x =1 

new_add(2,3)      # TypeError: add() takes 2 positional arguments but 3 were given
new_add(2,y=3)    # TypeError: add() got multiple values for argument 'y'
new_add(x=2,y=3)  # TypeError: add() got multiple values for argument 'x'

传递的func参数不一定必须是函数,只要实现了__call__方法

class Demo:

    def __call__(self,x):
        return x
    
par_obj = partial(Demo(),100)
print(par_obj()) #输出100

传递的func参数为partial对象,链式调用

def test(a,b,c,d):
    print(a,b,c,d)


obj1 = partial(test,1)

obj2 = partial(obj1,2)

obj3 = partial(obj2,c=3)

# obj3(4)#报错,参数c重复,还是要根据函数的参数逻辑来,关键字参数要在位置参数之后

obj3(d=4)  # 输出 1 2 3 4

走一边源码流程

    def __new__(cls, func, /, *args, **keywords):
        if not callable(func):
            raise TypeError("the first argument must be callable")

        if hasattr(func, "func"):
            args = func.args + args
            keywords = {**func.keywords, **keywords}
            func = func.func

        self = super(partial, cls).__new__(cls)

        #创建一个实例属性 保存原来的函数
        self.func = func
        # 保存调用partial时候 传进来的位置参数
        self.args = args
        # 保存调用partial时候 传进来的关键字参数
        self.keywords = keywords
        return self
    def __call__(self, /, *args, **keywords):
        keywords = {**self.keywords, **keywords}
        return self.func(*self.args, *args, **keywords)

obj1 的 func = test函数, self.args = (1,) kwargs={}

obj2 的func = obj1,因为if hasattr(obj1, "func")为true,所以self.args=obj1.args + 2,self.args = (1,2),kwargs={} 然后func = func.func原始的函数test赋值过来

同理 obj3 的self.args = (1,2),self.kwargs={'c':3}

所以obj3(d=4) 相当于 *self.args为(1,2) **self.keywords为{'c':3} **keywords为{'d':4}keywordsz再组合成{'c':3,'d':4}, 最后输出结果为 1 2 3 4

标签:__,partial,函数,self,args,func,keywords
来源: https://www.cnblogs.com/alantammm/p/16540607.html