(转)Python偏函数functools.partial的理解

原文:https://blog.csdn.net/Deft_MKJing/article/details/102788883

前言
什么是偏函数?这名字是谁取的,我保证不打死他。。。。。。
Python两个关键的知识点,一个装饰器,我们上一篇已经有介绍了,下一个就是偏函数,俗称partial function,不过把他叫做辅助函数,就好理解多了。
借助python的help函数查看下:

可以看到partial函数接收三个参数:

第一个参数是函数,可以是你自定义的,也可以是内置的
可变参数*args 需要被固定的位置参数
**kwargs关键字参数 如果原函数中关键字不存在,将会扩展,如果存在,就覆盖
作用:

partial函数的作用就是:将所作用的函数作为partial()函数的第一个参数,原函数的各个参数依次作为partial()函数的后续参数,原函数有关键字参数的一定要带上关键字,没有的话,按原有参数顺序进行补充。

偏函数的使用
偏函数的第二部分,可变位置参数,按照原有的函数参数顺序进行补充,参数将作用在原函数上,最后偏函数返回一个新函数,和装饰器的区别在于,装饰器对于函数进行二次包装,产生特殊效果,但又不同于装饰器,偏函数产生了一个新函数,而装饰器,可改变被装饰函数的函数入口地址而不影响原函数。

案例1(定义个sum函数,计算可变值,加上10或者20更多)
装饰器实现

import functools

def sum_extention(*args1): # 带上参数的装饰器
def wrapper(func):
@functools.wraps(func) # 这句话 把原函数被decrations修饰后,属性值重新赋回
def decrations(*args2): # 此之外层都是装饰器,这里的参数要原函数保持一致
s = 0
for x in args1:
s += x # 新装饰值结果
return func(*args2) + s # 我们再原函数上加上装饰的值
return decrations
return wrapper # 返回实际装饰器


@sum_extention(100,200)
def sum(*args):
s = 0
for x in args:
s += x
return s

print(sum(1,2,3,4,5))
print(sum.__name__)
# 日志
315
sum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sum(1,2,3,4,5)的结果变成了315,这就是加了装饰器的意义。用装饰器实现了扩展功能后,最后原函数的属性任然是sum,因此在扩展的基础上,@functools.wraps(func)生效,完全不影响我们的原函数。

偏函数partial实现

def sum(*args):
s = 0
for x in args:
s += x
return s


sum_ex = functools.partial(sum, 100,200)
print(sum)
print(sum_ex)

print(sum_ex(1,2,3,4,5))
print(sum.__name__)
# 日志
<function sum at 0x103fad9d8>
functools.partial(<function sum at 0x103fad9d8>, 100, 200)
315
sum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以清楚的看到,一个是原函数地址,另一个是偏函数产生的新函数的地址,通过对比,产生的函数,实际是包含原函数的地址入口的,区别在于,这个新函数的地址,还有一个参数,100,200,相当于提前绑定固定参数,包了一层函数,简化原函数调用,产生一个新的函数地址,原来是类还是类,原来是函数就还是函数。

在看一个例子:

import re
import functools

def my_search_func(text):
is_space_together = functools.partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = functools.partial(re.search, '[a-zA-Z]\=')

if is_space_together(text):
print('等号前面有空格')
elif is_grouped_together(text):
print('等号前面无缝连接')
else:
print('随他吧')


my_search_func('my name is mikejing')
my_search_func('a = 1')
my_search_func('a+b=3')
# 日志
随他吧
等号前面有空格
等号前面无缝连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可以感受到偏函数就是一种对函数的辅助,你可以加工任何函数或者类,提前绑定参数,然后重新定义一个更加清晰的函数方便自己使用。

当函数的参数个数太多时,使用functools.partial可以创建一个新的函数,这个新函数或者新类可以固定原函数的部分参数,从而调用起来更加简单。

偏函数配合装饰器使用
当类当做装饰器的时候,如果需要传参,类的结构参数会发生变化,上一篇文章有讲到,因此我们这引入了偏函数进行参数绑定,简化了装饰器的传参

import functools
import time


class DelayMan(object):
def __init__(self, durations, func):
super().__init__()
self.durations = durations
self.func = func

def __call__(self, *args, **kwargs):
print('please waite for %s seconds...' % self.durations)
time.sleep(self.durations)
return self.func(*args, **kwargs)

def no_delay_man(self, *args, **kwargs):
print('call immediately 。。。。')
return self.func(*args, **kwargs)


def delay(durations):
return functools.partial(DelayMan, durations)


@delay(5)
def add(*args):
return sum(args)


print(add(1, 2, 3, 4, 5, 6))
print(add.no_delay_man(2))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
可以看到这里的装饰器@delay返回的是一个用偏函数定义的新类,这个类不再是DelayMan,而是包了一层,里面有DelayMan类名,而且已经先绑定了类名和durations,以至于当我们调用装饰的时候,传入durations即可,装饰器默认会再最后传入被装饰的func,因此DelayMan的初始化参数中会有一个func用来接收,最后由于类是callable的,就完成了装饰。这种就是偏函数和装饰器最经典的配合。

框架内使用案例分析
from django.contrib.admin import sites这个模块内有个@never_cache装饰器,打开继续看

def never_cache(view_func):
@wraps(view_func)
def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
add_never_cache_headers(response)
return response
return _wrapped_view_func
1
2
3
4
5
6
7
该函数装饰是在response的Header里面加入no-cache字段,这里我们不关注这个,我们看下还有个内层装饰器@wraps(view_func),看过上一篇文章,基本上就知道这个是干嘛的,继续打开源码

def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function

Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
1
2
3
4
5
6
7
8
9
10
11
12
13
在外部调用@wraps(view_func),传入被最外层被装饰的函数,比如logout函数,但是为了在外部调用logou.__name__还是维持原样,就需要在调用这个,可以看到wraps内部实现也是用了偏函数,锁定update_wrapper函数的三个参数

def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
1
2
3
4
可以看到,wraps首先锁定了wrapped,assigned,updated三个参数,最后我们装饰的是_wrapped_view_func,也就是最后的参数传入wrapper接收,打开最终源码

def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
......省略几行
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper
1
2
3
4
5
6
7
8
9
这里的wrapper是_wrapped_view_func,wrapped是logout,可以看到源码里面,通过getattr把_wrapped_view_func的函数签名属性,全部替换成logout的原函数的值,这也就是类似KVO的实现,隐藏了具体的实现,除非你自己Debug才能看到猫腻,外部看到的和原来没什么区别。

总结
按我们个人的理解

这里的偏函数可以用作@wraped,系统自带的函数,对函数进行辅助冻结参数,方便外部调用,只要传入func即可
还有一个就是类装饰器带参数的时候,用偏函数辅助冻结类的参数,方便外部装饰的时候只要传入关键参数即可,而且不用改变类的调用方式。
官网的理解:

functools.partial(func, *args, **keywords) Return a new partial object
which when called will behave like func called with the positional
arguments args and keyword arguments keywords. If more arguments are
supplied to the call, they are appended to args. If additional keyword
arguments are supplied, they extend and override keywords. Roughly
equivalent to:

简单翻译: 它返回一个偏函数对象,这个对象和 func 一样,可以被调用,同时在调用的时候可以指定位置参数 (*args) 和 关键字参数(**kwargs)。如果有更多的位置参数提供调用,它们会被附加到 args 中。如果有额外的关键字参数提供,它们将会扩展并覆盖原有的关键字参数。它的实现大致等同于如下代码:

# 定义一个函数,它接受三个参数
def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
1
2
3
4
5
6
7
8
9
10
The partial() is used for partial function application which “freezes”
some portion of a function’s arguments and/or keywords resulting in a
new object with a simplified signature. For example, partial() can be
used to create a callable that behaves like the int() function where
the base argument defaults to two:

比如,partial() 可以用于合建一个类似 int() 的函数,同时指定 base 参数为2,代码如下:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
1
2
3
4
5
一句话概括如下:
partial() 是被用作 “冻结” 某些函数的参数或者关键字参数,同时会生成一个带有新标签的对象(即返回一个新的函数或者类)。

参考文章:
彻底了解 Python partial()
偏函数基础
Python进阶之路:偏函数functools.partial的应用
Python中*args 和**kwargs的用法
————————————————
版权声明:本文为CSDN博主「Deft_MKJing宓珂璟」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Deft_MKJing/article/details/102788883

原文地址:https://www.cnblogs.com/liujiacai/p/15590844.html