Python—装饰器

一 装饰器

1.1 什么是装饰器

器:指的是具备某一功能的工具

装饰:指的是为被装饰器对象添加新功能

装饰器就是用来为被装饰器对象添加新功能的工具
注意:装饰器本身可以是任意可调用对象,被装饰器的对象也可以是任意可调用对象

2.2 为何要用装饰器

软件的设计应该遵循开放封闭原则,既对功能扩展是开放的,而对修改是封闭的。

二 装饰器的实现

函数装饰器分为:无参装饰器和有参装饰器两种,二者的实现原理一样,都是'函数嵌套+闭包+函数对象'的组合使用的产物。

装饰器的实现必须遵循两大原则:
        1. 不修改被装饰对象的源代码
        2. 不修改被装饰器对象的调用方式
        

如果想为下述函数添加统计其执行时间的功能

import time

def download_movie():
    print('开始下载电影...')
    # 模拟电影下载时间3秒
    time.sleep(3) # 等待3秒
    print('电影下载成功...')
download_movie() # 函数执行

遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样

start_time = time.time() # 获取当前时间戳
download_movie()
end_time = time.time() # 获取当前时间戳
print(f'消耗时间:{end_time - start_time}')

考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入

def time_record(func):
    #统计开始
    start_time = time.time()
    res = func()
    stop_time = time.time()
    print(f'消耗时间:{end_time - start_time}')
    return res

但之后函数的调用方式都需要统一改成

time_record(download_movie)
time_record(其他函数)

这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下:

def time_record(func):
    def inner(): # 引用外部作用域的变量func
        #统计开始
        start_time = time.time()
        res = func()
        stop_time = time.time()
        print(f'消耗时间:{end_time - start_time}')
        return res
    return inner

这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要实现执行一次time_record将被装饰的函数传入,返回一个闭包函数inner重新赋值给变量名/函数名download_movie,如下:

download_movie = timer_record(download_movie) 
# 得到download_movie = inner ,inner携带对外作用域的引用,func = 原始的download_movie
download_movie
# 执行的是inner(),在函数inner的函数体内再执行最原始的download_movie

至此我们便实现了一个无参装饰器time_record,可以在不修改被装饰对象download_movie源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数时一个有参函数,便会报出异常

# 当为有参函数时我们就可以将以上装饰器修正为:
def time_record(func):
    def inner(*args,*kwargs): #'*args,*kwargs'组合的使用便满足被装饰函数参数的所有情况
        start_time = time.time()
        res = func(*args,*kwargs)
        stop_time = time.time()
        print(f'消耗时间:{end_time - start_time}')
        return res
    return inner
        

此时我们就可以用time_record来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代download_movie = timer_record(download_movie)的形式,需要在被装饰对象的正上面单独一行添加@time_record,当解释器解释到@time_record时就会调用time_record函数,且把他正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名

@time_record # download_movie = timer_record(download_movie)
def download_movie():
    print('开始下载电影...')
    # 模拟电影下载时间3秒
    time.sleep(3) # 等待3秒
    print('电影下载成功...')

如果我们有多个装饰器,可以叠加多个

def wrapper1(func):
    def inner(*args,**kwargs):
        print('1---start')
        # 被装饰对象在调用时,如果还有其他装饰器,会先执行其他装饰器中的inner
        res = func(*args,**kwargs)
        print('1---end')
        return res
    return inner1
    
    
def wrapper2(func):
    def inner(*args,**kwargs):
        print('2---start')
        # 被装饰对象在调用时,如果还有其他装饰器,会先执行其他装饰器中的inner
        res = func(*args,**kwargs)
        print('2---end')
        return res
    return inner2   
    
    
 def wrapper3(func):
    def inner(*args,**kwargs):
        print('3---start')
        # 被装饰对象在调用时,如果还有其他装饰器,会先执行其他装饰器中的inner
        res = func(*args,**kwargs)
        print('3---end')
        return res
    return inner3
'''
叠加装饰器的装饰顺序与执行顺序:
        装饰顺序:调用wrapper装饰器拿到返回值inner
        由下往上装饰(*****)
        
        执行顺序:调用装饰过后的返回值inner
        由上往下执行(*****)
 '''       

@wrapper1 # index<<---inner1 = wrapper1(inner2)
@wrapper2 # inner2 = wrapper2(inner3)
@wrapper3 # inner3 = wrapper3(index)
def index(): # 被装饰对象
    print('from index...')
# 比如用很多张纸包装礼物的时候,必定是从里到外包装,而拆开的时候必定是从外到里。
上一篇:day3


下一篇:day3