最近看到一些Python装饰器相关的库都使用到了 functools.partial
函数,不是很清楚使用的原因,探求一二.
functools.partial
偏函数,这个好理解,一般用于固定函数的部分参数,返回一个 partial
对象.
不过写出这个定义之后,有点能理解一定的关联性了,都是做函数的再次包装.
先来看看函数的使用
import functools
def mul(a, b):
return a * b
mul3 = functools.partial(mul, b=3)
mul(1, 2)
mul3(1)
我们看一下函数的定义 functools.partial(func, /, *args, **keywords)
当被调用时,行为类似于 func
附带位置参数 args
和关键字参数 keywords
被调用如果为调用提供了更多的参数,它们会被附加到 args
如果提供了额外的关键字参数,它们会扩展并重载 keywords
偏函数的优势
空间分离:可以在代码的不同地方指定参数,从结构化的角度或者词法作用域的角度来说可能会很方便. 时间分离:参数可以在不同阶段被应用,表现上看来就是有些参数是包括在代码里的,另外一些参数是在运行时决定的.
partial对象
查看官方文档的解释,非常的详细了
partial
对象是由 partial()
创建的可调用对象。 它们具有三个只读属性:
partial.func
一个可调用对象或函数。 对 partial
对象的调用将被转发给 func
并附带新的参数和关键字。
partial.args
最左边的位置参数将放置在提供给 partial
对象调用的位置参数之前。
partial.keywords
当调用 partial
对象时将要提供的关键字参数。
partial 对比 function
partial
对象与 function
对象的类似之处在于它们都是可调用、可弱引用的对象并可拥有属性。但两者也存在一些重要的区别。例如前者不会自动创建 __name__
和 __doc__
属性。 而且,在类中定义的 partial
对象的行为类似于静态方法,并且不会在实例属性查找期间转换为绑定方法。
装饰器Decorator
装饰器其实只是一个语法糖,它本身就是一个函数,然后将函数作为参数传给它,并返回一个新的函数.
对于装饰器语法,第一个参数会默认传入被包装的函数关于装饰器本身就不多解释,这里主要说一下和 partial
相关的部分.
带参数的装饰器
我们先看下面的例子
@decorator_with_args('a', 'b')
def test(*args, **kwargs):
pass
上述包装过的函数实际上会被翻译成 test = decorator_with_args(arg1, arg2)(test)
decorator_with_args
是一个接受自定义参数的函数,并且返回实际的装饰器.
from functools import wraps
def decorator_with_args(arg1, arg2):
print "arg1 is %s, arg2 is %s" % (arg1, arg2)
@wraps(func)
def real_decorator(func):
def wrapper(*args, **kwargs):
print "call real function"
return func(*args, **kwargs)
return wrapper
return real_decorator
在实际的执行逻辑上,在装饰器引入的时候,便会先执行最外层的参数函数 decorator_with_args
然后返回装饰器,等待调用执行.
partial出场
partial在装饰器场景算是一个小的 trick
, 可以实现一个带可选参数的装饰器,并且符合编程习惯,能在不传参数的时候通过 @decorator
也可以传递参数给它,比如 @decorator(a, b)
这里选用 Python Cookbook 里面的一个示例:
from functools import wraps, partial
import logging
def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
if func is None:
return partial(logged, level=level, name=name, message=message)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
# Example use
@logged
def add(x, y):
return x + y
@logged(level=logging.CRITICAL, name='example')
def spam():
print('Spam!')
可以看到, @logged
装饰器可以同时不带参数或带参数。
展开第一个装饰器得到
add = logged(add)
展开第二个装饰器得到
spam = logged(level=logging.CRITICAL, name='example')(spam)
接着根据 logged
的定义继续展开
spam = partial(logged, level=logging.CRITICAL, name='example')(spam)
这时候 partial(logged, level=logging.CRITICAL, name='example')
会返回一个装饰器, 这个装饰器的作用就是 logged
的真正装饰逻辑
这里使用 partial
的逻辑是,先将 logged(level=logging.CRITICAL, name='example')
视为一个函数调用,然后返回真实的装饰器,走后面的装饰器流程.
partial
会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了。
我们通过自己实现这个装饰器来对比 partial
的作用
from functools import wraps, partial
import logging
def my_logged(level=logging.DEBUG, name=None, message=None):
def decorator(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorator
partial
带来的好处是这个装饰器是既可以带参数调用,也可以不带参数调用的,同时也减少了装饰器的包装层级,代码看起来更加清晰.
这里给出一个 partial
装饰器模板
import functools
def decorator_with_args(func=None, *, arg=None):
# 将参数传给自身返回带参数的装饰器
if func is None:
return functools.partial(decorator_with_args, arg=arg)
# 处理参数的逻辑
# 实际的装饰器
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 编写装饰器的逻辑
# 处理真实函数调用前的逻辑
# ...
result = func(*args, **kwargs)
# 处理真实函数调用后的逻辑
# ...
return result
return wrapper
参考链接
https://stackoverflow.com/questions/48098569/use-of-functools-partial-in-a-decorator-that-attaches-function-as-attribute-of-o https://python3-cookbook.readthedocs.io/zh%5FCN/latest/c09/p06%5Fdefine%5Fdecorator%5Fthat%5Ftakes%5Foptional%5Fargument.html