超详细Python装饰器函数讲解 | 从此不再遗忘

前言

装饰器函数(wrapper)是Python的一个非常强大的特性,笔者在初次学习时认为这种特性过于无关紧要,甚至对于这种脱裤子放屁的行为感到匪夷所思(还是太年轻了hh),而后续在学习flask以及其他高阶Python技能的过程中发现经常使用到,从而每次遇见装饰器不是苦不堪言就是即将苦不堪言。因此,决定写下这篇文章,以一种通俗的方式帮助理解,从此彻底克服装饰器恐惧症~

一、什么是以及为什么要用装饰器

初次看到装饰器这个词时,你可能会有疑问:什么是装饰器?为什么要用装饰器?

如果从一种咬文嚼字的角度来看,所谓装饰,就是对于物品进行美化或者修饰打扮的行为,就比如你要去参加一场面试或者约会,你可能会尽可能地想办法弄身不错的行头或者再找tony老师临时凹个造型使得你的形象更加得体美观,这就是装饰。

可能有人不禁发问,这些废话和Python装饰器有什么关系?我们观察上述行为,不难发现不管是前者还是后者,都是在事物的表层进行美化,而并没有对内核进行修改(你换身衣服之后还是你),而这,就是装饰器的作用:在不对原有的函数代码作任何变动的情况下,对函数增加新的内容。装饰器函数的本质依然还是一个函数,它只是把原来的那个函数当作是一个参数使用,并返回一个新的函数。不对已存在代码进行更改即可完成功能增加这一特性则完美符合开放封闭原则,这便是装饰器之用。

二、怎么实现装饰器

可能就算这样解释,理解起来还是有一些抽象,那么下面这个简单的装饰器案例或许能够帮助理解什么叫返回一个新的函数~

def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    func1 = oxk3
    func1()
def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    func1 = oxk3  #将oxk3赋值给func1
    func1()
    print('\n')
    blog(func1)   #将func1作为参数传入blog

执行结果:
`

尽管简单,但这个装饰器已能够让我们清楚其工作原理。在这个装饰器案例中,我们在blog函数中设置参数name,在blog函数中又使用name()把参数作为函数使用,而oxk3函数则成为了传入blog函数的参数。

三、装饰器和语法糖@

如果你已经积累了一定程度的Python学习经历,你可能看过一些使用到@符号跟上一段参数的场景,而这个@就是我们所谓的语法糖,那么这个语法糖究竟有什么作用,我们为什么要用语法糖?

接下来看一个案例:

如果我想测试oxk3函数的执行时间,我们可以对上面的代码进行如下修改

import time

def timer(name):
    def inner():
        start = time.time()
        name()
        print(time.time() - start)
    return inner

def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3 = timer(oxk3)
    oxk3()

执行结果:

这里我们定义了一个timer函数用来计算出函数的执行时间,然后将oxk3作为参数传入timer,最后再将timer(oxk3)的结果赋给oxk3,这样一来我们就可以在不对oxk3函数进行变动的情况下,在每次运行时测出时间。

但是,加入我们要给一万个函数加上这个功能,那岂不是需要编写一万次函数名 = timer(函数名),这样就显得很麻烦了。于是,我们加入语法糖来提升效率。

import time

def timer(name):
    def inner():
        start = time.time()
        name()
        print(time.time() - start)
    return inner

def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

@timer
def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3()

执行结果和之前没有变化:

不难理解,语法糖的作用就是把跟在它后面的函数自动的执行一次函数名 = 语法糖内容(函数名),所以我们可以用一个伪代码来定义语法糖的作用:

@语法糖内容 == 函数名 = 语法糖内容(函数名)

这样一来,我们后期维护的效率就会大大提升!

四、装饰带参数的函数

此时,你已经了解了装饰器的含义和语法糖的作用,那我再给你提一个需求:使用timer函数装饰blog函数测出blog(oxk3)的执行时间。可能你会一时摸不着头脑,这里就需要引入两个新的参数*args和**kwargs用来传递任何参数。

1.*args称作可变位置参数

作用:接收任意数量的非关键字参数(又称位置参数),将其打包成元祖传入

def sum_values(*args):
    return sum(args)
print(sum_values(1, 2, 3))  # 输出6

此处args的类型为元祖(1, 2, 3)

2.**kwargs称作可变关键字参数

作用:接收任意数量的关键字参数(如key=value的键值对形式),并打包成字典

def print_profile(**kwargs):
    for k, v in kwargs.items():
        print(f"{k}: {v}")
print_profile(name="Alice", age=30)  # 输出name: Alice, age: 30

此处kwargs的类型为字典(‘name’: ‘Alice’, ‘age’: 30)

现在,让我们对之前的代码进行修改,实现我的需求。

import time

def timer(name):
    def inner(*args, **kwargs):
        start = time.time()
        name(*args, **kwargs)
        print(time.time() - start)
    return inner

@timer
def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3()
    print('----------------')
    blog(oxk3)

执行结果:

当然,既然是传参,你也可以规划得更加直截了当一点,要传什么就写什么,使用*args和**kwargs只是为了通过动态的方式让传参更加万能。

如果我们直接写明参数:

import time

def timer(name):
    def inner(a):
        start = time.time()
        name(a)
        print(time.time() - start)
    return inner

@timer
def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3()
    print('----------------')
    blog(oxk3)

执行结果:

完全没有问题~

五、wraps装饰器

再次回到这个案例

import time

def timer(name):
    def inner():
        start = time.time()
        name()
        print(time.time() - start)
    return inner

def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3 = timer(oxk3)
    oxk3()

现在,我们来思考一个问题,当我们执行完oxk3 = timer(oxk3)之后,我们执行的oxk3函数还是最初的oxk3函数吗?

为了得知答案,我们可以在末尾加一行代码来查看函数名称

print(oxk3.__name__) 

执行结果:

我们可以看到,当oxk3被装饰后,它已经成为了被我们定义在timer中的inner函数,以一个新的函数身份出现,但是这样就有可能导致后续的调试与维护产生一定差错,我们可以使用wraps装饰器解决这个问题。

导入wraps装饰器

wraps装饰器的作用是在使用装饰器的场景中,保留被装饰函数的初始数据,使用@wraps可以确保装饰后的函数更像原始函数。

from functools import wraps
import time

def timer(name):
    @wraps(name)
    def inner():
        start = time.time()
        name()
        print(time.time() - start)
    return inner

def blog(name):
    print('进入blog')
    name()
    print('已进入blog')

def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':
    oxk3 = timer(oxk3)
    oxk3()
    print(oxk3.__name__) 

执行结果:

六、带参数装饰器

在很多时候,我们可能需要让被修饰的函数在使用时互相之间产生差异性来方便识别或者满足其他需求,而正常情况下对同一个函数如果我们需要得到不同的结果,只需传入不同的参数即可,而装饰器函数也是函数,带参数装饰器函数能有效增强装饰器的灵活性和活动性。

继续使用之前的案例,我们让装饰器接受参数,体现出差异性:

import time

def decorator(info=None):
    def timer(name):
        def inner(*args, **kwargs):
            start = time.time()
            name(*args, **kwargs)
            print(f"正在计算[{info}]执行时间....")
            print(f"[{info}]的执行时间为:", time.time() - start)
        return inner
    return timer

@decorator(info="blog")
def blog():
    print('进入blog')
    print('这里是明石同学oxk3')
    print('已进入blog')

@decorator(info="oxk3")
def oxk3():
    print('这里是明石同学oxk3')

if __name__ == '__main__':

    oxk3()
    blog()

执行结果:
在这里插入图片描述

总结

在这篇文章中,我使用一个极其简单的案例深入浅出地讲解了从装饰器定义到装饰器的各种功能用法,希望看完这篇文章的你,从此成为装饰器高手~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值