前面一篇文章介绍了python装饰器,最后引入了functools.wraps的使用,本篇文章将对它进行深入的探究。

functools模块提供了一系列的高阶函数以及对可调用对象的操作,其中为人熟知的有reduce,partial,wraps等。

为了保证被装饰器装饰后的函数还拥有原来的属性,wraps通过partial以及update_wrapper来实现。

我们先来了解一下partial,partial用于部分应用一个函数,它基于一个函数创建一个可调用对象,把原函数的某些参数固定,调用时只需要传递未固定的参数即可。

我们先看partial如何使用:

import functools

def add(a, b):
print(a + b) add = functools.partial(add, 1)
add(2) 输出:3

add函数原本接收两个参数a和b,经过partial包装之后,a参数的值被固定为了1,新的add对象(注意此处add已经是一个可调用对象,而非函数,下文分析源码会看到)只需要接收一个参数即可。

通俗点说:就是把原函数的部分参数固定了初始值,新的调用只需要传递其它参数。

下面来分析partial的源码(Python3.7),只摘录了核心部分:

class partial:
"""New function with partial application of the given arguments
and keywords.
""" __slots__ = "func", "args", "keywords", "__dict__", "__weakref__" def __new__(*args, **keywords):
if not args:
raise TypeError("descriptor '__new__' of partial needs an argument")
if len(args) < 2:
raise TypeError("type 'partial' takes at least one argument")
cls, func, *args = args
if not callable(func):
raise TypeError("the first argument must be callable")
args = tuple(args) if hasattr(func, "func"):
args = func.args + args
tmpkw = func.keywords.copy()
tmpkw.update(keywords)
keywords = tmpkw
del tmpkw
func = func.func self = super(partial, cls).__new__(cls) self.func = func
self.args = args
self.keywords = keywords
return self def __call__(*args, **keywords):
if not args:
raise TypeError("descriptor '__call__' of partial needs an argument")
self, *args = args
newkeywords = self.keywords.copy()
newkeywords.update(keywords)
return self.func(*self.args, *args, **newkeywords)

通过重写“_new__”方法,自定义对象实例化过程。

1、元组拆包,获取到传入的原函数(func)和需要固定的参数(args)

cls, func, *args = args

2、主要是为了支持嵌套调用,即add=partial(partial(add,1),2)这种情况,可先看第三步,回过头再来看

if hasattr(func, "func"):
args = func.args + args
tmpkw = func.keywords.copy()
tmpkw.update(keywords)
keywords = tmpkw
del tmpkw
func = func.func

3、实例化partial对象,将传入的函数和参数设置为当前对象的属性

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

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

到这里我们已经明白了partial是怎么保存原函数和固定参数的了,下面来看一下调用的时候是如何执行的。

先简单了解一下可调用对象:当一个类实现了"__call__"方法后,这个类的对象就能够像函数一样被调用。

class Callable:
def __call__(self, a, b):
return a + b func = Callable()
result = func(2, 3) # 像函数一样调用
print(result) 输出:5

好啦,我们看下partial的"__call__"是如何实现的:

    def __call__(*args, **keywords):
if not args:
raise TypeError("descriptor '__call__' of partial needs an argument")
self, *args = args
newkeywords = self.keywords.copy()
newkeywords.update(keywords)
return self.func(*self.args, *args, **newkeywords)

1、元组拆包,获取到传入的非固定参数args

self, *args = args

2、拷贝当前对象的keywords参数,并且合并传入的非固定参数字典

newkeywords = self.keywords.copy()
newkeywords.update(keywords)

3、调用当前对象的func属性,func即被partial包装的原函数,同时传入暂存的固定参数self.args以及新传入的其它参数。

至此一切真相大白:partial通过实现"__new__"和"__call__"生成一个可调用对象,这个对象内部保存了被包装函数以及固定参数,这个对象可以像函数一样被调用,调用时,其实是执行了对象内部持有的被包装函数,其参数由固定参数和新传入的参数组合而来。

继续探索wraps的源码:

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)

入参解读:

wrapped:指被装饰器装饰的原函数,我们的装饰器便是要拷贝它的属性。

assigned:要被重新赋值的属性列表,默认为WRAPPER_ASSIGNMENTS,可自定义传入

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')

updated:要被合并的属性列表,默认为WRAPPER_UPDATES,可自定义传入

WRAPPER_UPDATES = ('__dict__',)

返回值:

返回了一个partial对象,这个对象对update_wrapper进行了包装,固定了wrapped,assigned,updated三个参数。

wraps本省就是一个装饰器,因为它返回的是一个“函数”即partial对象,这个对象接收函数作为参数,同时以函数作为返回值。

接下来看update_wrapper:

def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
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

这个便是解析@functools.wraps(func)时最底层执行的逻辑,代码很简洁,就是把wrapped函数的属性拷贝到wrapper函数中。

wrapped是被装饰的原函数

wrapper是被装饰器装饰后的新函数。

通过下面的例子对执行过程和参数进行对号入座

def outer(func):
@functools.wraps(func)
def inner(*args, **kwargs):
print(f"before...")
func(*args, **kwargs)
print("after...") return inner @outer
def add(a, b):
"""
求和运算
"""
print(a + b)

1、原函数为add。

2、@outer会去执行outer装饰器,传入add函数,返回一个inner函数。

3、执行outer函数时,加载inner函数,此时会直接执行functools.wraps(func)返回一个可调用对象,即partial对象。

4、此时inner的装饰器实际上是@partial,partial会被调用,传入inner函数,执行partial内部的update_wrapper函数,将func的相应属性拷贝给inner函数,最后返回inner函数。这一步并没有生成新的函数,仅仅是改变了inner函数的属性。

5、把add指向inner函数。

6、调用add实际调用的是inner函数,inner函数内部持有原add函数的引用即func。

update_wrapper函数参数对应:

wrapper指的是inner函数

wrapped指的是func即原始的add函数

总结:

1)functools.wraps 旨在消除装饰器对原函数造成的影响,即对原函数的相关属性进行拷贝,已达到装饰器不修改原函数的目的。

2)wraps内部通过partial对象和update_wrapper函数实现。

3)partial是一个类,通过实现了双下方法new,自定义实例化对象过程,使得对象内部保留原函数和固定参数,通过实现双下方法call,使得对象可以像函数一样被调用,再通过内部保留的原函数和固定参数以及传入的其它参数进行原函数调用。

Python 装饰器解析(二)的更多相关文章

  1. python 装饰器(二): 加参数

    接上篇python 闭包&装饰器(一) 一.功能函数加参数:实现一个可以接收任意数据的加法器 源代码如下: def show_time(f): def inner(*x, **y): # 形参 ...

  2. python装饰器(二)

    有参装饰器 def outer(flag): def timer(func): def inner(*args,**kwargs): if flag: print('''执行函数之前要做的''') r ...

  3. Python 装饰器Decorator(二)

    对于上一篇“”Python闭包“”随笔中提到的make_averager()函数的如下实现,我们把历史值保存在列表里,每次计算平均值都需要重新求和,当历史值较多时,需要占用比较多的空间并且效率也不高. ...

  4. python 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明

    变量作用域规则 在示例 7-4 中,我们定义并测试了一个函数,它读取两个变量的值:一个是局部变量 a,是函数的参数:另一个是变量 b,这个函数没有定义它. >>> def f1(a) ...

  5. (转)python装饰器二

    Python装饰器进阶之二 保存被装饰方法的元数据 什么是方法的元数据 举个栗子 def hello(): print('Hello, World.') print(dir(hello)) 结果如下: ...

  6. Python学习:11.Python装饰器讲解(二)

    回顾 上一节我们进行了Python简单装饰器的讲解,但是python的装饰器还有一部分高级的使用方式,这一节就针对python装饰器高级部分进行讲解. 为一个函数添加多个装饰器 今天,老板又交给你一个 ...

  7. 我终于弄懂了Python的装饰器(二)

    此系列文档: 1. 我终于弄懂了Python的装饰器(一) 2. 我终于弄懂了Python的装饰器(二) 3. 我终于弄懂了Python的装饰器(三) 4. 我终于弄懂了Python的装饰器(四) 二 ...

  8. Python装饰器实例讲解(二)

    Python装饰器实例讲解(二) Python装饰器实例讲解(一) 你最好去看下第一篇,虽然也不是紧密的链接在一起 参考B站码农高天的视频,大家喜欢看视频可以跳转忽略本文:https://www.bi ...

  9. python装饰器的详细解析

    什么是装饰器? python装饰器(fuctional decorators)就是用于拓展原来函数功能的一种函数,目的是在不改变原函数名(或类名)的情况下,给函数增加新的功能. 这个函数的特殊之处在于 ...

  10. Python装饰器、迭代器&生成器、re正则表达式、字符串格式化

    Python装饰器.迭代器&生成器.re正则表达式.字符串格式化 本章内容: 装饰器 迭代器 & 生成器 re 正则表达式 字符串格式化 装饰器 装饰器是一个很著名的设计模式,经常被用 ...

随机推荐

  1. R语言—数据基础及练习

    ## 创建leadership数据框 manager <- c(1,2,3,4,5) date <-c("10/24/08","10/28/08", ...

  2. [转帖]10.2 Data Collector与MDW

    10.2 Data Collector与MDW Data Collection功能是SQL SERVER 2005版本提供的数据库监控报表的功能,通过定时地对数据库的语句运行情况,服务器各种资源的监控 ...

  3. [转帖]Active Session History (ASH)

    Introduction V$ACTIVE_SESSION_HISTORY DBA_HIST_ACTIVE_SESS_HISTORY Enterprise Manager Performance Pa ...

  4. [转帖]使用 EXISTS 代替 IN 和 inner join

      在使用Exists时,如果能正确使用,有时会提高查询速度: 1,使用Exists代替inner join 2,使用Exists代替 in 1,使用Exists代替inner join例子: 在一般 ...

  5. [转帖]arm linux下编译xtrabackup-2.4.5

    环境:aarch64/centos7.6  glibc-2.17 编译器:gcc version 5.5.0 (GCC) 官方参考文档:https://www.percona.com/doc/perc ...

  6. 人大金仓学习之二_ksh和kddm的学习

    人大金仓学习之二_ksh和kddm的学习 摘要 承接上一篇文章 主要是这里总结一下ksh相关的文档. 这里学习了很多文档: https://help.kingbase.com.cn/v8/perfor ...

  7. 【转帖】32.MinorGC、MajorGC和FullGC的对比

    目录 1.MinorGC.MajorGC和FullGC的对比 2.GC触发机制 1.MinorGC.MajorGC和FullGC的对比 1.JVM在进行GC的时候,并不是每次都是对新生代.老年代.永久 ...

  8. Python学习之三: 编译二进制

    Python学习之三: 编译二进制 摘要 每次使用python 执行py文件其实是比较麻烦的 主要是还得安装python的虚拟机,以及安装对应的pip包. 感觉比较繁杂 理论上最快捷的方式是编译成 二 ...

  9. [转帖]Redis Scan 原理解析与踩坑

    https://www.cnblogs.com/jelly12345/p/16424080.html 1. 概述由于 Redis 是单线程在处理用户的命令,而 Keys 命令会一次性遍历所有 Key, ...

  10. 简单进行Springboot Beans归属模块单元的统计分析方法

    简单进行Springboot Beans归属模块单元的统计分析方法 背景 基于Springboot的产品变的复杂之后 启动速度会越来越慢. 公司同事得出一个结论. beans 数量过多会导致启动速度逐 ...