【python】37_高级特性之装饰器

【摘要】上篇博文介绍了闭包,闭包还有一个常用场景就是装饰器。因此,本篇博文将介绍装饰器

1.装饰器

试想一下这个场景,如果我们现在有一份代码,可以给用户提供打游戏、看视频,但是产品经理突然告知我们要对用户进行年龄判断,如果是未成年人,则这两个程序不对用户执行;如果是成年人,则执行相应程序。那么,难道我们要改代码吗?
又或者,我们需要在每一次的节日都打印该节日快乐,难道要频繁更改代码内容吗?
在Python中,针对这一情况,我们可以使用装饰器来予以解决。

1.1 装饰器的概念

装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景。

1.2 为何使用装饰器

有一些场景,需要频繁更改逻辑,但是遵循 开放封闭 原则,虽然在这个原则是用于面向对象开发,但是也适用于函数式编程。简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展。即:
封闭:已实现的功能代码块
开放:对扩展开发

装饰器可以帮助我们:
1).快速的在不改变原有函数的基础上,添加逻辑信息
2).不改变函数的调用方式

来看一个简单的例子(无参数的函数),假如下面这两个就是已经实现好的播放视频以及游戏代码块

def videos():
    print("正在播放视频.....")

def game():
    print("正在进入游戏.....")

现在我们要对未成年人进行限制,并且不能在封装好的代码块中修改。所以我们只能使用装饰器,而装饰器其实就是闭包的一个应用场景,那肯定要满足闭包的三个条件;

age = 13
#1). 外部函数里面也定义了一个函数
def is_adult(fun):
    def wrapper():
        if age >= 18:
        	2).内部函数调用了外部函数的临时变量
            fun()
        else:
            print("未成年")

    # 3).返回的是函数的引用
    return wrapper

上面的代码就是我们所说的装饰器。那么如何使用装饰器呢?

#用装饰器is_adult装饰函数videos,并将返回只wrapper赋值给videos函数
videos=is_adult(videos)
#调用videos函数——>实质上就是调用wrapper函数内部代码
videos()

由于上面age变量的值为13,因此在调用wrapper函数后,执行了对age的判断,<18打印了未成年
在这里插入图片描述
如果把age变量赋值为35

age = 35

再调用上面的代码

videos=is_adult(videos)
videos()

即:
在这里插入图片描述
则执行效果如下:
在这里插入图片描述
但是这样装饰函数,实属麻烦。python中的语法糖可以给我们提供同样的作用,帮我们装饰需要装饰的函数,其语法如下:(只需要在代码块上面添加:@装饰器名称)

@is_adult   #######语法糖所执行的效果就是:videos = is_adult(videos)
def videos():
    print("正在播放视频.....")

@is_adult  #######语法糖所执行的效果就是:game = is_adult(game)
def game():
    print("正在进入游戏.....")

再来一个听音乐的代码块,没有用装饰器装饰

def music():
    print("正在播放音乐.....")

我们现在给变量age赋值13,然后依次调用这三个函数,执行效果如下:
在这里插入图片描述
如果变量age=19,执行效果如下:
在这里插入图片描述
这是最简单的装饰器例子,无参数的函数装饰器。在下面的例子中,我们还需要掌握被装饰的函数有参数、被装饰的函数有可变参数和关键字参数、被多个装饰器装饰的函数,最后总结出通用装饰器的模板。

2.装饰器的应用场景

2.1 插入日志

日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用,借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述(比如:每个事件发生时的数据都是不同的)
模块logging的使用具体可参考官方文档link.
日志的函数如下:
在这里插入图片描述
日志的级别如下:
在这里插入图片描述
简单的例子介绍一下logging的使用:

import logging
#日志的基本配置
"""
对 basicConfig() 的调用应该在 debug() , info() 等的前面。因为它被设计为一次性的配置,只有第一次调用会进行操作,随后的调用不会产生有效操作。
"""
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',
                    level=logging.INFO,filename='example.log')
# 要更改用于显示消息的格式,你需要指定要使用的格式format,其中levelname (严重性), message (事件描述,包括可变数据),asctime(时间)
"""
如果多次运行上述脚本,则连续运行的消息将追加到文件 example.log 。 
如果你希望每次运行重新开始,而不是记住先前运行的消息,则可以通过将上例中的调用更改为来指定 filemode 参数为w。因为filemode的默认值是a
"""
logging.debug('This message should appear on the console111')
logging.info('So should this111')
logging.warning('And this, too111')

由于之前已经打印过内容在example.log中,并且在basicConfig中没有指定filemode=‘w’,因此会在原本的example.log中追加info()和warning()内容。
在这里插入图片描述

下面讲一下插入日志的装饰器如何下:
首先我们若使用loggin模块,先对basicConfig做配置。

import logging
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',
                    level=logging.DEBUG,filename='example.log',
                    filemode='w',datefmt='%m/%d/%Y %I:%M:%S %p')
"""
日期/时间显示的默认格式类似于 ISO8601 或 RFC 3339 。 
如果你需要更多地控制日期/时间的格式,请为 basicConfig 提供 datefmt 参数,
"""
#日志配置格式也可以这样写format = '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'

然后写一个名为add_log的装饰器。

def add_log(func):
	""""插入日志的装饰器"""
    def wrapper(*args,**kwargs):    #形参,args是元组,kwargs是字典
        logging.debug('%s program starts' %(func.__name__))
        result = func(*args,**kwargs)    #实参,*args,**kwargs是解包
        logging.debug('%s program stops' %(func.__name__))
        return result
    return wrapper

然后装饰在一个很简单无参数的login函数上。

@add_log
def login():
    print('登陆成功')

我们调用login()函数,产生了example.log文件,其中记载了我们调用login函数所产生的日志信息。
在这里插入图片描述
但是这里要注意一个点,我们知道调用login函数,实质上是执行wrapper里面的内容,因此如果我们要打印出login的函数名,结果是wrapper。

print(login.__name__)

在这里插入图片描述
如果要保持被装饰函数func原有的函数名和帮助文档时,我们需要用到一个内置装饰器wraps。全部代码如下:

from functools import wraps
def add_log(func):
    """
    插入日志的装饰器
    """
    @wraps(func)   #保持func原有的函数名和帮助文档,
                   #否则被装饰函数的函数名和帮助文档是wrapper
    def wrapper(*args,**kwargs):
        """
        闭包函数wrapper
        """
        logging.debug('%s program starts' %(func.__name__))
        result = func(*args,**kwargs)
        logging.debug('%s program stops' %(func.__name__))
        return result
    return wrapper

@add_log
def login():
    """
    login函数
    """
    print('登陆成功')


login()

print(login.__name__)
print(login.__doc__)

打印结果如下:
在这里插入图片描述
下面介绍一个不使用logging模块的插入日志的装饰器:
首先分析,日志要显示的内容有:当前时间、主机名、当前正在运行的程序名称、运行结果
我们的思路是:利用time模块获取当前时间,利用os模块获取主机名,再用sys模块获取程序名称,最后对这些字段进行拼接。装饰器内容如下:

def add_log(fun):
    def wrapper(*args, **kwargs):  
        # 获取被装饰的函数的返回值
        result = fun(*args, **kwargs)     
        # 返回当前的字符串格式时间
        now_time = time.ctime()
        # 获取主机名 nodename='foundation0.ilt.example.com'
        hostname = os.uname().nodename.split('.')[0]
        # 获取运行的程序
        process_full_name = sys.argv[0]
        process_name = os.path.split(process_full_name)[-1]
        # 日志内容
        # 获取函数名: 函数名.__name__
        info ="函数[%s]的运行结果为%s" %(fun.__name__, result)
        log = " ".join([now_time, hostname, process_name, info])
        print(log)
        return  result
    return  wrapper

def wrapper(*args, **kwargs)这一行代码中的 *args, **kwargs表示接收函数调用时的任意传参。其中 *args接收到的是一个元组, **kwargs接收到的是一个字典。
fun(*args, **kwargs)是将接收到的参数解包。

获取主机名的代码为什么那样写,可以看下图,就会比较明晰:
在这里插入图片描述
以及获取当前正在运行的程序名代码实现:
在这里插入图片描述
装饰器写好后,我们来装饰其他函数
在这里插入图片描述
在register函数上面使用语法糖,调用add_log这个装饰器来装饰register函数。所作的流程如注释。即在第42行调用register函数时,其实是将该函数中的参数传给装饰器中的wrapper函数,并执行。
定义wrapper函数时,参数 *args, **kwargs接收所有任意参数,接收成功后,调用fun(*args, **kwargs)执行register函数体,函数体是打印参数(name、age、province、gender)的值,并将返回值赋给result。
再执行warpper函数体下面的内容。

2.2 性能测试

下面我们写一个可计算程序运行速度的装饰器。

from functools import wraps
import time

def timeit(func):     #step2:timeit(download_music)
    """计算程序运行时间的装饰器"""
    @wraps(func) #保持被装饰函数的原有函数名和帮助文档
    def wrapper(*args,**kwargs):     #step5 :wrapper('yellow','D:/Music')
        """闭包函数wrapper"""
        start_time = time.time()
        result = func(*args,**kwargs)   #step6:func=download_music -->download_music('yellow','D:/Music')
        end_time = time.time()
        print('%s运行时间为%.3f' %(func.__name__,end_time-start_time))
        return result
    return wrapper            #setp3:download_music = wrapper

@timeit      #step1:download_music = timeit(download_music)   -->   setp3:download_music = wrapper
def download_music(name,dir_path):    #step7
    """下载音乐的函数"""
    time.sleep(0.8)
    print('%s已下载至%s' %(name,dir_path))

download_music('yellow','D:/Music')  #step4
print('download_music的函数名:',download_music.__name__)
print('download_music的帮助文档:',download_music.__doc__)

结果如下:
在这里插入图片描述

2.3 事务处理

例如我们要写一个将所有数据转换成json格式的装饰器。

from functools import wraps
import string
import json

def json_dumps(func):    #step2:json_dumps(get_userInfo)  -->wrapper
    @wraps(func)
    def wrapper(*args,**kwargs):  #step5
        result = func(*args,**kwargs)  #step6:result = get_userInfo
        return json.dumps(result)
    return wrapper

@json_dumps               #step1:get_userInfo = json_dumps(get_userInfo)  -->  step3:get_userInfo=wrapper
def get_userInfo():       #step7
    userInfo = {'user'+item:'passwd'+item for item in string.digits}
    return userInfo

result = get_userInfo()  #step4
print(type(result))
print(result)

结果如下:
在这里插入图片描述

2.4 缓存

functools.lru_cache的作用主要是用来做缓存,把相对耗时的函数结果进行保存,避免传入相同的 参数重复计算。同时,缓存并不会无限增长,不用的缓存会被释放。

"""fib的递归写法"""
def fib2(num):
    if num in (1,2):
        return 1
    else:
        return fib2(num-2)+fib2(num-1)

from functools import lru_cache
"""fib的递归写法但是加了cache"""
@lru_cache(maxsize=10000)
def fib3(num):
    if num in (1,2):
        return 1
    else:
        return fib3(num-2)+fib3(num-1)

@timeit     
def use_cache():
    result = fib3(40)
    print(result)

@timeit
def no_cache():
    result = fib2(40)
    print(result)

use_cache()
no_cache()

执行结果:
在这里插入图片描述
可以明显看出使用了缓存后的fib数列加载的就很快。下面我们自己写一个fib数列的缓存装饰器。

def fib_cache(func):
    """自定义的fib数列缓存的装饰器"""
    cache = {1:1,2:1,3:2,4:3}
    def wrapper(num):
        if num in cache:   #若要调用的num在缓存中,则直接返回num对应的value值
            return cache.get(num)
        else:                #若在缓存中没有对应的num,则执行func(即fib函数),将得到的value值存入cache并返回
            result = func(num)
            cache[num] = result
            return result
    return wrapper

@fib_cache
def fib4(num):
    """加了自定义fib_cache装饰器的函数"""
    if num in (1,2):
        return 1
    else:
        return fib4(num-2)+fib4(num-1)
        

def fib2(num):
	"""没有缓存的fib函数"""
    if num in (1,2):
        return 1
    else:
        return fib2(num-2)+fib2(num-1)

@timeit
def use_cache():
    result = fib4(40)
    print(result)

@timeit
def no_cache():
    result = fib2(40)
    print(result)


use_cache()
no_cache()

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

3.通用装饰器的模板

掌握下面这个模板,装饰器就稳了~只需要在里面添加内容即可。

def decorate(fun):
    def wrapper(*args, **kwargs):    # args, kwargs是形参
    	# 在函数之前做的操作
        result = fun(*args, **kwargs)  # *args, **kwargs是实参, *args, **kwargs是在解包
        # result接收被装饰函数的返回值;
        # 在函数之后添加操作
        return result
    return wrapper
   
#@装饰器的名字 
@decorate    ===> add=decorate(add)  ---> add指向wrapper函数位置
def add():
    return 'ok'

#调用被装饰的函数
add()

4.多个装饰器装饰

上面我们看了一个装饰器装饰一个函数的例子,如果是多个装饰器装饰一个函数,执行流程如何呢?
例子一
这里有两个装饰器,分别为:1). 判断用户是否登录?2). 判断用户是否有权限?

def is_login(fun):
    def wrapper1(*args, **kwargs):
        print("判断是否登录......")
        result = fun(*args, **kwargs)
        return  result
    return  wrapper1

def is_permission(fun):
    def wrapper2(*args, **kwargs):
        print("判断是否有权限......")
        result = fun(*args, **kwargs)
        return  result
    return  wrapper2

用这两个装饰器装饰函数delete()

@is_login   
@is_permission
def delete():
   print('正在删除学生信息')
    return  '删除学生信息完毕'

执行被装饰的delete(),用result接收delete()的返回值,并打印。

result = delete()
print(result)

执行效果如下:
在这里插入图片描述

用这个简单的例子,来分析一下执行流程:
1). delete = is_perssion(delete) # delete实际上是wrapper2
先用下面这个装饰器is_permission装饰delete函数,装饰完之后,delete函数指向wrapper2。
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete实际上是wrapper1
再用上面的装饰器is_login装饰delete函数(此时的delete函数其实是wrapper2),装饰完wrapper2函数后返回给delete,delete函数此时指向wrapper1

上面这两步是装饰过程,装饰完成之后,再调用detele函数
3). delete() —> wrapper1() —> wrapper2() —> delete()
此时调用delete函数其实就相当于调用wrapper1,进入wrapper1函数体内执行print(“判断是否登录…”),再执行 result = fun(*args, **kwargs),但其实这个fun指的是wrapper2函数,因此这句调用执行wrapper2函数体。
先执行print(“判断是否有权限…”),再执行 result = fun(*args, **kwargs)。这个fun才是真正的delete函数,执行delete函数体内容,并将delete函数的返回值获取到赋值给wrapper2中的 result,wrapper2函数体执行完毕后将返回值result带回给wrapper1函数体中的result,执行完wrapper1中的函数体将返回值result带回给调用语句result = delete()的result,最后打印这个返回值。

嗯,是有点绕,如果分析不出来其执行流程的话,记住以下两句话:
1.装饰过程: 函数先被下面的装饰器装饰,后被上面的装饰器装饰
2.执行过程:上面装饰器先执行, 下面的装饰器后执行

例子二
假设我们要执行删除学生信息的程序,但是要求1)用户登陆成功 2)用户拥有权限
下面看一下代码:
首先系统用户的信息应该存储在数据库中,用户登陆时获取到用户登陆信息,与数据库中的信息作匹配,这里我们用字典存储系统中所有用户的信息。只有两个用户root和admin。

db = {
    'root': {
        'name': 'root',
        'passwd': '123',
        'is_super': 1  # 0-不是 1-是
    },
    'admin': {
        'name': 'admin',
        'passwd': '456',
        'is_super': 0  # 0-不是 1-是
    }
}

再用一个空字典,来存储当前登陆用户的信息

login_user_session = {}

下面是装饰器is_login,用来判断用户是否登录, 如果没有登录,先登录

def is_login(fun):
    def wrapper1(*args, **kwargs):
        if login_user_session:
            result = fun(*args, **kwargs)
            return result
        else:
            print("请您登陆".center(50, '*'))
            user = input("User: ")
            passwd = input('Password: ')
            if user in db:
                if db[user]['passwd'] == passwd:
                    login_user_session['username'] = user
                    print('登录成功')
                    # ***** 用户登录成功, 执行删除学生的操作;
                    result = fun(*args, **kwargs)
                    return result
                else:
                    print("密码错误")
            else:
                print("用户不存在")

    return wrapper1

第二个装饰器是is_permission,用来判断用户是否有权限进行删除操作。

def is_permission(fun):
    def wrapper2(*args, **kwargs):
        print("判断是否有权限......")
        current_user = login_user_session.get('username')
        permissson = db[current_user]['is_super']
        if permissson == 1:
            result = fun(*args, **kwargs)
            return result
        else:
            print("用户%s没有权限" % (current_user))

    return wrapper2

用这两个装饰器来装饰函数delete。

@is_login 
@is_permission
def delete():
    return "正在删除学生信息"

装饰过程是:
1). delete = is_permission(delete) # delete = wrapper2
先用装饰器 is_permission装饰delete函数,装饰完后,delete指向wrapper2
2). delete = is_login(delete) # delete = is_login(wrapper2) # delete = wrapper1
再用装饰器 is_login装饰delete函数(实质上是wrapper2),装饰完后,delete指向wrapper1

调用函数:

result = delete()
print(result)

被调用的过程是:
delete( ) ------> wrapper1( ) —> wrapper2( ) —> delete( )
执行delete( ),实际上是执行wrapper1( )的函数体,wrapper1中的fun参数是接收到的wrapper2函数,因此执行wrapper2;warpper2( )函数体中的fun参数是delete函数,执行完后,再一步一步将返回值带回。最后打印。
执行效果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.带参数的装饰器

如果装饰器需要传递参数, 在原有的装饰器外面嵌套一个函数即可。
直接看例子,假如我们这里对用户登陆有限制,只能本地用户登陆,不支持远程用户登录,可以通过装饰器接收参数,来分别执行。
以下为装饰器,注意要在外部嵌套一个函数

def auth(type):
    def wrapper1(fun):
        def wrapper(*args, **kwargs):
            if type=='local':
                user = input("User:")
                passwd = input("Passwd:")
                if user == 'root' and passwd == '123':
                    result = fun(*args, **kwargs)
                    return result
                else:
                    print("用户名/密码错误")
            else:
                print("暂不支持远程用户登录")

        return wrapper

    return wrapper1

用这个装饰器auth来装饰home函数

@auth(type='remote')
def home():
    print("这是主页")

这里要注意的是:上面我们语法糖的结构是@funName
这里的结构是
@funName()
。默认跟的不是函数名, 先执行该函数, 获取函数返回结果,再 @funNameNew
理解过程如下:
@后面跟的是装饰器函数的名称, 如果不是名称, 先执行,再和@结合。
1). @auth(type=‘local’)
2). wrapper1 = auth(type=‘local’)
3). @wrapper1
4). login = wrapper1(login)
4). login = wrapper2

一定要注意:
@函数名 add=函数名(add)
@函数名( ) @新的函数名
调用函数看效果:

home()

在这里插入图片描述
如果auth装饰器中的参数是local,执行结果为:
在这里插入图片描述
在这里插入图片描述
【官方装饰器案例】
https://wiki.python.org/moin/PythonDecoratorLibrary

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值