python:functools.partial和functools.wraps使用
1 前言
python内置的functools模块,提供了一些非常好用的类或者方法,其中functools.partial和functools.wraps的使用频率较高,本文将针对其分析使用。
2 使用
2.1 functools.partial
functools.partial是提供的工具类,python源码如下(本文参考自python3.9源码):
class partial:"""New function with partial application of the given argumentsand 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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(partial, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywordsreturn selfdef __call__(self, /, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)@recursive_repr()def __repr__(self):qualname = type(self).__qualname__args = [repr(self.func)]args.extend(repr(x) for x in self.args)args.extend(f"{k}={v!r}" for (k, v) in self.keywords.items())if type(self).__module__ == "functools":return f"functools.{qualname}({', '.join(args)})"return f"{qualname}({', '.join(args)})"def __reduce__(self):return type(self), (self.func,), (self.func, self.args,self.keywords or None, self.__dict__ or None)def __setstate__(self, state):if not isinstance(state, tuple):raise TypeError("argument to __setstate__ must be a tuple")if len(state) != 4:raise TypeError(f"expected 4 items in state, got {len(state)}")func, args, kwds, namespace = stateif (not callable(func) or not isinstance(args, tuple) or(kwds is not None and not isinstance(kwds, dict)) or(namespace is not None and not isinstance(namespace, dict))):raise TypeError("invalid partial state")args = tuple(args) # just in case it's a subclassif kwds is None:kwds = {}elif type(kwds) is not dict: # XXX does it need to be *exactly* dict?kwds = dict(kwds)if namespace is None:namespace = {}self.__dict__ = namespaceself.func = funcself.args = argsself.keywords = kwds
根据源码,我们以同样的逻辑定义part类(功能类似partial):
def add(x, y):return x + yclass part: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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(part, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywordsreturn selfdef __call__(self, /, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)print(hasattr(add, 'func'))
p = part(add, 10)
print(p(7))
效果和functools.partial类似,执行结果如下:
去掉__new__方法和__call__方法的“/,”和上述效果一致(去掉位置参数*args前加上的“/,”):
class part: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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(part, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywordsreturn selfdef __call__(self, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)
执行结果不变:
但是注意,如果将“/,”替换为“_,”就会占据解构的位置参数,导致缺少参数而执行异常(python不像javascript,javascript可以使用空格加逗号(如[ , , a] = [1, 2, 3])来解构不需要的值,但是python一般是通过"_“来解构不需要的值,实际值是被”_"所占据了的(如 [ _, _, a] = [1, 2, 3])):
class part: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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(part, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywordsreturn selfdef __call__(self, _, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)
执行报错(不能使用“_,”):
上面的part类,参考的functools.partial实现,核心是自定义实现__new__方法,达到自定义对象实例self的效果,同时为self添加属性func,即我们传入的函数,以及为self添加属性args和keywords,也就是上面我们的part(add, 10),除了第一个函数参数外,传入的固定的位置参数和关键字参数。同时第二个核心点是自定义实现__call__方法,__call__方法会使得对象成为callable类型的对象,也就是可以像函数一样调用,即func(),也就是当我们使用类名()来生成类的实例对象时,对实例对象再次调用实例对象()时,会调用__call__方法。
__new__方法的了解可参考:python:__new__和__init__
下面是__call__方法的定义和使用示例:
class CallableClazz:def __call__(self, a, b):return a + bfunc = CallableClazz()
result = func(4, 6) # 实例对象,可以像函数一样调用,因为有实现__call__方法
print(result)
print(callable(func))
# 10
# True
由此可知,根据自定义实现__call__方法,解构关键字参数,即keywords = {**self.keywords, **keywords},self.keywords是我们固定的关键字参数,而keywords是后续调用函数对象,也就是上述调用p(7)中传入的关键字参数;同理,self.func(*self.args, args, **keywords)中传入的self.args, *args,self.args也是固定的位置参数,args是调用p(7)中传入的位置参数,这里是7,所以可以达到固定参数值的情况下,执行函数。
小结:partial通过实现"__new__“和”__call__"生成一个可调用对象,这个对象内部保存了被包装函数以及固定参数,这个对象可以像函数一样被调用,调用时,其实是执行了对象内部持有的被包装函数,其参数由固定参数和新传入的参数组合而来。
2.2 functools.wraps
有了上述functools.partial的源码分析,再参考python源码functools.wraps如下:
def wraps(wrapped,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES):"""Decorator factory to apply update_wrapper() to a wrapper functionReturns a decorator that invokes update_wrapper() with the decoratedfunction as the wrapper argument and the arguments to wraps() as theremaining arguments. Default arguments are as for update_wrapper().This is a convenience function to simplify applying partial() toupdate_wrapper()."""return partial(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)
这里我们需要提前知道一个知识点,也就是python的装饰器中,def的函数中返回inner函数,同时def定义的函数的接收参数亦是函数,就可以达到装饰器的效果。而若inner函数是callable的,也就是类似类实现了__call__方法,那么也可以作为类似inner函数的效果,形成python装饰器。
由此可知,上述的wraps函数,实际是python的装饰器,因为返回的是callable的类实例partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated),该类实例还可以执行:类实例(),也就是实现了__call__方法,可以类似函数调用,所以亦可充当装饰器。
那么接下来分析其效果:
返回的partial实例对象,第一个参数是update_wrapper,其为函数,python functools源码如下:
def update_wrapper(wrapper,wrapped,assigned = WRAPPER_ASSIGNMENTS,updated = WRAPPER_UPDATES):"""Update a wrapper function to look like the wrapped functionwrapper is the function to be updatedwrapped is the original functionassigned is a tuple naming the attributes assigned directlyfrom the wrapped function to the wrapper function (defaults tofunctools.WRAPPER_ASSIGNMENTS)updated is a tuple naming the attributes of the wrapper thatare updated with the corresponding attribute from the wrappedfunction (defaults to functools.WRAPPER_UPDATES)"""for attr in assigned:try:value = getattr(wrapped, attr)except AttributeError:passelse:setattr(wrapper, attr, value)for attr in updated:getattr(wrapper, attr).update(getattr(wrapped, attr, {}))# Issue #17482: set __wrapped__ last so we don't inadvertently copy it# from the wrapped function when updating __dict__wrapper.__wrapped__ = wrapped# Return the wrapper so this can be used as a decorator via partial()return wrapper
而partial实例对象的第二个参数是wrapped,也就是装饰器修饰的原本函数对象;第三个参数是assigned,如下源码所示:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
partial实例对象的第四个参数是updated,源码如下:
WRAPPER_UPDATES = ('__dict__',)
所以实际返回的partial实例对象,是对方法update_wrapper,固定了位置参数
根据functools工具类的源码,我们实现下述的功能并分析效果:
class part: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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(part, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywordsreturn selfdef __call__(self, /, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
WRAPPER_UPDATES = ('__dict__',)def wrapsNew(wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES):"""Decorator factory to apply update_wrapper() to a wrapper functionReturns a decorator that invokes update_wrapper() with the decoratedfunction as the wrapper argument and the arguments to wraps() as theremaining arguments. Default arguments are as for update_wrapper().This is a convenience function to simplify applying partial() toupdate_wrapper()."""return part(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)def update_wrapper(wrapper,wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES):"""Update a wrapper function to look like the wrapped functionwrapper is the function to be updatedwrapped is the original functionassigned is a tuple naming the attributes assigned directlyfrom the wrapped function to the wrapper function (defaults tofunctools.WRAPPER_ASSIGNMENTS)updated is a tuple naming the attributes of the wrapper thatare updated with the corresponding attribute from the wrappedfunction (defaults to functools.WRAPPER_UPDATES)"""for attr in assigned:try:value = getattr(wrapped, attr)except AttributeError:passelse:setattr(wrapper, attr, value)for attr in updated:getattr(wrapper, attr).update(getattr(wrapped, attr, {}))# Issue #17482: set __wrapped__ last so we don't inadvertently copy it# from the wrapped function when updating __dict__wrapper.__wrapped__ = wrapped# Return the wrapper so this can be used as a decorator via partial()return wrapperdef outer(func):@wrapsNew(func)def inner(*args, **kwargs):print(f"before...")func(*args, **kwargs)print("after...")return inner@outer
def add(a, b):"""求和运算"""print(a + b)
对于add方法,我们执行如下:
if __name__ == '__main__':print(add(1, 6))
执行前分析如下:
对于代码:
@outer
def add(a, b):"""求和运算"""print(a + b)
根据装饰器语法糖的形式,实际是如下的效果:
add = outer(add)
意即:相当于于我们调用的add方法,是outer(add)方法,即outer(add)(1, 6)
outer方法如下:
def outer(func):@wrapsNew(func)def inner(*args, **kwargs):print(f"before...")func(*args, **kwargs)print("after...")return inner
那么继续分析:
而outer(add) 方法的返回值,相当于: inner = wrapsNew(add)(inner) return inner
即: outer(add) = wrapsNew(add)(inner)
即: outer(add) = part(update_wrapper, wrapped=wrapped,assigned=assigned, updated=updated)(inner)
即: add = part(update_wrapper, wrapped=add方法,assigned=assigned, updated=updated)(inner)
上述拆解分析后可知:
- wrapped是 add 方法,也就是我们原生定义的add方法,不加任何修饰改变的
- assigned是 WRAPPER_ASSIGNMENTS = (‘__module__’, ‘__name__’,‘__qualname__’, ‘__doc__’, ‘__annotations__’)
- updated是 WRAPPER_UPDATES = (‘__dict__’,)
也就是说,add经装饰后的新方法,是通过我们自定义的part类绑定了3个关键字参数(kwargs),然后传入参数outer.inner并调用得来的,关键字参数如下:
- wrapped(add定义的原方法);
- assigned,即(‘__module__’, ‘__name__’, ‘__qualname__’,‘__doc__’, ‘__annotations__’);
- updated,即(‘__dict__’,);
所以下述执行__new__方法时,func是update_wrapper方法,而self.args为空元组,self.keywords是关键字参数字典dict,值也就是上述提到的绑定的3个关键字参数:
class part: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 + argskeywords = {**func.keywords, **keywords}func = func.funcself = super(part, cls).__new__(cls)self.func = funcself.args = argsself.keywords = keywords
然后执行part(…)(inner)的时候,就会调用如下part类的call方法:
def __call__(self, /, *args, **keywords):keywords = {**self.keywords, **keywords}return self.func(*self.args, *args, **keywords)
debug结果如下可见,keywords就是上述的3个关键字参数:
args是调用的part(…)(inner)传入的inner方法:
而self.args因为调用part类时没有传入需要固定的位置参数,所以是空元组:
也就是说,上述的调用,最终表现形式如下:
update_wrapper(
inner方法(位置参数),
{wrapped: add原方法,
assigned: ('\_\_module\_\_', '\_\_name\_\_', '\_\_qualname\_\_','\_\_doc\_\_', '\_\_annotations\_\_'),
updated: ('\_\_dict\_\_',)}(关键字参数))
故而如下参数中,wrapper是inner方法,wrapped是add原方法:
def update_wrapper(wrapper,wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES):
所以核心逻辑是update_wrapper方法:
def update_wrapper(wrapper,wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES):for attr in assigned:try:value = getattr(wrapped, attr)except AttributeError:passelse:setattr(wrapper, attr, value)for attr in updated:getattr(wrapper, attr).update(getattr(wrapped, attr, {}))# Issue #17482: set __wrapped__ last so we don't inadvertently copy it# from the wrapped function when updating __dict__wrapper.__wrapped__ = wrapped# Return the wrapper so this can be used as a decorator via partial()return wrapper
debug可知,下述逻辑,是把wrapped原方法(也就是@wrapsNew装饰器的方法参数,这里是add方法)的一些assigned属性获取到(即’__module__‘, ‘__name__’, ‘__qualname__’,’__doc__', ‘__annotations__’),然后将结果赋值给wrapper方法(也就是@wrapsNew装饰的方法inner):
__module__属性:
__name__属性:
__qualname__属性:
__doc__属性:
也就是add方法的doc:
__annotations__属性:
然后针对@wrapsNew装饰的方法inner的__dict__属性进行更新:
inner的__dict__属性:
add的__dict__属性:
for attr in updated:getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
将inner方法的属性更新部分为wrapped方法的属性后,最后将wrapper(inner)方法的__wrapped__属性设置为wrapped(add)方法:
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
最后返回wrapper(inner)方法:
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
到此全部分析已完成接下来就是执行如下逻辑,func就是wrapped方法,add:
def add(a, b):"""求和运算"""print(a + b)
def inner(*args, **kwargs):print(f"before...")func(*args, **kwargs)print("after...")
最终执行结果如下:
上述可知,原方法我们也可以获取并执行,通过装饰后方法.__wrapped__获取到原方法并执行:
print(add.__wrapped__(2, 8))
结果:
10
None
同时,装饰后的add方法的一些属性,也被更新为原方法add的属性,这样更加贴合原有方法的语义:
print(add.__module__)
print(add.__name__)
print(add.__qualname__)
print(add.__doc__)
print(add.__annotations__)
print(add.__dict__)
结果如下:
若为add方法增加annotation信息(注意:python的annotation仅提示作用,不是强制类型限制,且和java的注解Annotation是完全不同,反编译可知java的注解Annotation是继承了Annotation接口的特殊接口),如下为add原方法的参数和返回值添加注解annotation:
@outer
def add(a: int, b: int) -> int:"""求和运算"""print(a + b)return a + b
执行如下:
结果如下,可见inner方法的annotation注解信息(inner方法实际是没有注解信息的),实际是使用的add方法的注解元信息:
获取的注解信息如下:
结果:
或者上述的annotation注解信息获取,可以使用inspect模块中的Signature对象,对可调用对象进行内省:
import inspectsignature = inspect.signature(add)
print(signature.parameters["a"])
print(signature.parameters["a"].annotation)
print(signature.parameters["a"].annotation == int)
print(signature.parameters["a"].annotation is int)
结果和上述类似:
再针对多个装饰器举个栗子:
import functoolsdef decorator1(func):@functools.wraps(func)def wrapper(*args, **kwargs):print('decorator 1 begin.')return func(*args, **kwargs)return wrapperdef decorator2(func):@functools.wraps(func)def wrapper(*args, **kwargs):print('decorator 2 begin.')return func(*args, **kwargs)return wrapper@decorator1
@decorator2
def add(x: int, y: int) -> int:print("开始累加:")print(x + y)if __name__ == '__main__':print("1**********\n")add(1, 2)print("2**********\n")add.__wrapped__(1, 2)print("3**********\n")add.__wrapped__.__wrapped__(1, 2)
执行结果如下:
注意分析如下代码:
@decorator1
@decorator2
def add(x: int, y: int) -> int:print("开始累加:")print(x + y)
其本质如下:
decorator1 = decorator1(add1)
注意这里的add1是下面的add2,而add2其实是decorator2(add):
add2 = decorator2(add)
所以公式如下:
add = decorator1(decorator2(add))
所以执行完,一定是decorator1先打印,再是decorator2打印。同时基于此,由上述我们可知,add可以多次调用__wrapped__方法。
add方法调用一次__wrapped__时,实际返回的是decorator1的包裹原方法,所以原方法是decorator2(add),故而执行可见,打印了:decorator 2 begin.语句。
而add方法连续调用两次__wrapped__时,实际返回的decorator2(add)的包裹原方法,也就是我们自定义的原方法add,故而decorator 1 begin.和decorator 2 begin.语句均未打印。
小结:
1)functools.wraps 旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,以达到装饰器不修改原函数的目的(保存函数的元数据,更加符合原方法语义、属性)。
2)wraps内部通过partial对象和update_wrapper函数实现。
3)partial是一个类,核心通过实现__new__和__call__方法,自定义实例化对象过程,使得对象内部保留原函数和固定参数,__call__方法使得对象可以像函数一样被调用,再通过内部保留的原函数和固定参数以及传入的其它参数进行原函数调用,wraps函数实现返回partial类实例,使得其可视作装饰器使用。