python:functools.partial和functools.wraps使用

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类实例,使得其可视作装饰器使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/707364.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

好易点 | 上海厨卫展首秀人气爆棚,智能阳台备受瞩目

2024年5月14日,第28届中国国际厨房、卫浴设施展览会(简称:上海厨卫展)在上海盛大开幕。作为厨卫行业的年度盛事,本次展会不仅汇聚了众多国内外知名品牌,更成为了展示前沿五金厨卫臻品的重要平台。其中&…

k8s 二进制安装 详细安装步骤

目录 一 实验环境 二 操作系统初始化配置(所有机器) 1,关闭防火墙 2,关闭selinux 3,关闭swap 4, 根据规划设置主机名 5, 做域名映射 6,调整内核参数 7, 时间同步 三 部署 dock…

图数据库原理在构建实体血缘关系图中的应用与实践

在当今复杂的数据驱动型应用中,理解和管理实体间的复杂关系变得日益重要。通过低代码平台进行配置的应用,因采用了DSL语言进行统一设计,要让专业开发者和非专业开发者都能快速实现复杂应用的构建,实体之间的数据逻辑和关系梳理就尤…

PyQt5 中的 List View

文章目录 1. 基础概念2. 创建 List View2.1 PyQt5 中一个简单的 List View 实例2.2 代码解释2.3 运行结果 3. 数据模型3.1 标准模型3.2 自定义模型 4. 自定义 List View4.1 使用样式表 (QSS)4.2 设置项委托 (Item Delegate) 5.事件处理6. 与数据交互6.1 添加数据6.2 删除数据6.…

【JAVA入门】Day05 - 面向对象

【JAVA入门】Day05 - 面向对象 文章目录 【JAVA入门】Day05 - 面向对象一、对象的设计和使用1.1 类和对象1.2 类的分类 二、封装三、private 关键字四、this 关键字五、构造方法六、JavaBean七、对象的内存图7.1 一个对象的内存图7.2 两个对象的内存图7.3 两个引用指向同一个对…

后仿真中的关于延时问题(如何指定延时模式)

了解到这里,大家可能会有个疑问:据我们常见,一个模块中不仅包含specify块,还会包含大量的门单元等语句。我们通常在specify块中会指定模块路径延迟, 模块内的路径单元(基本的门级单元、开关级元件或者子模块)又存在分布延迟,。 模块中这两种延迟经常会共存,那么此时进行仿真…

Cadence 16.6 PCB Edito如何将鼠标中键反向拉拽改为正向拖拽

Cadence 16.6 PCB Editor如何将鼠标中键反向拉拽改为正向拖拽 Cadence 16.6 PCB Editor默认鼠标中键是反向的拉拽,让我很不适应,在网上找的可行的方法,在这里总结出来,希望能帮到其他工程师。 按照以下步骤进行操作: …

TypeScript基础知识:TypeScript是什么?为什么会出现TypeScript?TypeScript相较于javascript有那些优势?

TypeScript(简称:TS)是JavaScript的超集(JS有的TS 都有)。 图解就是: TypeScriptType javaScript(在JS 基础之上,为JS 添加了类型支持)。 TypeScript 是微软开发的开源编程语言,可以在任何运行JavaScript的地方运行。 那么为什么要出现type…

LLMjacking:针对云托管AI大模型服务的新型攻击

Sysdig威胁研究团队(TRT)观察到一种新型攻击,命名为LLMjacking。它利用窃取的云凭证,对托管在云上的十个大型语言模型(LLM)服务发起攻击。 这些凭证是从一个流行的目标获得,即运行着一个存在漏洞的Laravel版本(CVE-2021-3129&…

Media Encoder 2024 for Mac:专业的音视频编码神器

Media Encoder 2024 for Mac,作为Mac用户的专业音视频编码工具,凭借其强大的功能和用户友好的界面,深受专业人士的喜爱。它支持将各种格式的音视频素材转换为多种流行格式,如MP4、MOV、AVI等,满足不同的播放和发布需求…

C# WinForm —— 16 MonthCalendar 介绍

1. 简介 可以选择单个日期,也可以选择一段日期,在选择时间范围上 比较适用,但不能跨月份选择日期范围 在直观上,可以快速查看、选择日期/日期范围 2. 常用属性 属性解释(Name)控件ID,在代码里引用的时候会用到,一般…

【Python探索之旅】字典

字典的基本特性 创建字典 修改字典 添加键值对 删除键值对 字典方法 遍历字典 完结撒花​ 前言 字典是 Python 中内建的一种具有弹性储存能力的数据结构,可存储任意类型对象,与序列使用整数索引不同,它使用键(key)进行索引。 通常任何不…