🍉关于python装饰器中的@functools.wraps(func)那点事

1,114 阅读5分钟

python装饰器中的@wraps(func)

本文正在参加「Python主题月」,详情查看 活动链接 🍉夏日炎热来块西瓜
引言:最近在复习python装饰器的时候,运行程序时候加了@wraps(func)和没加@wraps(func)跑出来的程序结果都一致,我就纳闷@wraps(func)这个修饰符到底是有什么作用的,于是就上网查了下资料。

1.函数装饰器

  • 代码中没有加入 @wraps(func)
def get_time(func):
    def myWrapper(*args,**kwargs):
    	print(time.time())
        return func(*args,**kwargs)
    return myWrapper
    
@get_time
def a():
    print("执行a函数")  
a()
print(a.__name__)
    # 返回值:
    # 1626856703.067642
    # 执行a函数
    # a.__name__:myWrapper
  • 加入 @wraps(func)
def get_time(func):
    @functools.wraps(func)
    def myWrapper(*args,**kwargs):
    	print(time.time())
        return func(*args,**kwargs)
    return myWrapper
@get_time
def a():
    print("执行a函数")  
a()
print(a.__name__)
    # 返回值:
    # 1626856703.067642
    # :执行a函数
    # a.__name__:a
❤️从上面两个例子中可以看出 @functools.wrags使添加了装饰器的函数名不变。而没有使用@functools.wrags的函数的函数名则发生了变化。(当然不仅仅是函数名发生了变化)

2.wrags()的内部实现

def wraps(wrapped,assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

这里我介绍下上面的几个参数:
  • wrapped:装饰器装饰的原函数
  • assigned: 要被重新赋值的属性列表
  • update:要被合并的属性列表

函数返回一个partial对象,其固定了wrappedassignedupdate三个字段,也就是说 wraps等价于partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated),接下来我们就来分析一下partial()函数.

3.探究partial()函数

partial()的大致功能

partial()函数:也就是偏函数,它是对原始函数的二次封装,是将现有函数的部分参数预先绑定为指定值,从而得到一个新的函数,该函数就称为偏函数。相比原函数,偏函数具有较少可变参数,从而降低了函数调用的难度,提高函数可读性。

例:

from functools import partial
#定义个原函数
def display(name,age):
    print("name:",name,"age:",age)
#定义偏函数,其封装了 display() 函数,并为 name 参数设置了默认参数
GaryFun = partial(display,name = 'Gary')
#由于 name 参数已经有默认值,因此调用偏函数时,可以不指定
GaryFun(age = 13) #注意该处必须指定age = xxx 否则会去匹配name

# 给第一个参数赋值,并且后续赋值时候不能进行修改
Cao = partial(display,'cao')
# 给其他参数赋值
Cao(11) # 注意该处必须只填其他的元素 

# 运行结果:name:Gary age:13  name:cao age:11

Cao('ccc',111)
# 结果值:TypeError: display() takes 2 positional arguments but 3 were given
GaryFun(13)
# 结果值:TypeError: display() got multiple values for argument 'name'

总结来说就是partial()将函数的一些参数给固定了,了解了partial()的功能,下面来看看partial()的内部实现。

❤️话不多说上源码:
class partial:
    __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)

上述代码是partial的核心代码:其中包括两个函数__new__()__call__().

其中__new__()先获取对应的函数以及参数

`cls, func, *args = args` # 获取对应函数以及一些参数
# 获取传入函数的一些参数
if hasattr(func, "func"):
            args = func.args + args  # 将当前的args和传入func中的args参数合并
            tmpkw = func.keywords.copy()  # 获取传入func中的keywords
            tmpkw.update(keywords)	# 合并
            keywords = tmpkw
            del tmpkw
            func = func.func
# 实例化partial对象,将传入的函数和参数设置为当前对象的属性
self = super(partial, cls).__new__(cls)
self.func = func  
self.args = args
self.keywords = keywords
return self

关于__new__()方法上面super(partial, cls).__new__(cls)cls是指类本身而self是指类的一个实例.

super(partial, cls).__new__(cls)该语句的意思是返回一个 partial()的实例.

__new__()的作用

__new__的作用:

依照Python官方文档的说法,__new__方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass

__call__()的作用

接下来说说__call__()这个函数:

__call__这个方法是让类变成可调用对象,而对于可调用对象都可以理解为 对象名.__call__()

此外用__call__()可以弥补hasattr()函数查找类的实例对象时无法判断指定名称是类属性还是类方法的问题

例:

class CLanguage:
    def __init__ (self):
        self.name = "1"
    def say(self):
        print("111")

clangs = CLanguage()
if hasattr(clangs,"name"):  # 判断clangs中是否有name这个属性或对象
    print(hasattr(clangs.name,"__call__"))  # False

if hasattr(clangs,"say"):
    print(hasattr(clangs.say,"__call__"))  # True

了解了__call__()这个函数的功能后,我们来看下partial()中的__call__()函数

def __call__(*args, **keywords):
        if not args:
            raise TypeError("descriptor '__call__' of partial needs an argument")
        # 元组拆包,获取传入的非固定元素args
        self, *args = args  
         # 拷贝当前对象的keywords参数
        newkeywords = self.keywords.copy() 
        # 更新keywords参数
        newkeywords.update(keywords)  
        # 调用当前对象的func(被传入的函数),同时传入暂存的固定参数self.args以及新传入的其他参数
        return self.func(*self.args, *args, **newkeywords) 

经过查看partial函数的内部实现,我们发现partial()函数通过__call__()__new__()两个函数实现将传入函数的部分参数与其本身固定参数结合。

结语

经过对wraps以及partial两个函数内部实现的查看,我们大致也了解到了其实现过程以及功能。本篇文章到此结束。