15、装饰器

一、装饰器介绍

  • 1.1 为什么要用装饰器
  • 1.2 什么是装饰器

二、装饰器的实现

  • 2.1 无参装饰器的实现
  • 2.2 有参装饰器的实现

一、装饰器介绍

为何要用装饰器

软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有的代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。

软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

什么是装饰器

‘装饰’代指为被装饰对象添加新的功能,‘器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改装饰器对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续使用。

提示:可调用对象有函数,方法或者类,此处我们单以函数为例,来介绍装饰器,并且被装饰的对象也是函数。

二、装饰器的实现

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

无参装饰器的实现

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

import time


def index():
    time.sleep(3)
    print('Welcome to the index page')
    return 200


index()  # 函数执行

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

start_time = time.time()
index()
stop_time = time.time()
print('run time is %s' % (stop_time-start_time))

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

def wrapper(func):  # 通过参数接收外部的值
    start_time = time.time()
    res = func()
    stop_time = time.time()
    print('run time is %s' % (stop_time-start_time))
    return res

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

wrapper(index)
wrapper(其他函数)

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

def timer(func):    
    def wrapper():  # 通过参数接收外部的值
        start_time = time.time()
        res = func()
        stop_time = time.time()
        print('run time is %s' % (stop_time-start_time))
        return res
    return wrapper

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

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

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

def home(name):
    time.sleep(2)
    print('Welcome to the home page', name)
    
home = timer(home)
home('egon')

# 抛出异常
TypeError: wrapper() takes 0 positional arguments but 1 was given

之所以会抛出异常,是因为home('egon')调用的其实是wrapper('egon'),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,使用上*args+**kwargs组合,于是修正装饰器timer如下

def timer(func):
    def wrapper(*args,**kwargs):  
        start_time = time.time()
        res = func(*args,**kwargs)
        stop_time = time.time()
        print('run time is %s' % (stop_time-start_time))
        return res
    return wrapper

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

@timer  # index = timer(index)def index():    time.sleep(3)    print('Welcome to the index page')    return 200@timer  # home = timer(home)def home(name):    time.sleep(2)    print('Welcome to the home page', name)

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

@deco3@deco2@deco1def index():    pass# 本质:index = deco3(deco2(deco1(index)))

有参装饰器的实现

了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下

def deco(func):    def wrapper(*args,**kwargs):        res=func(*args,**kwargs)  # 编写基于文件的认证,认证通过则执行,并返回res    return wrapper

如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下

def deco(func):
    def wrapper(*args,**kwargs):
        if driver == 'file':
            res=func(*args,**kwargs)  # 编写基于文件的认证,认证通过则执行,并返回res
        elif driver == 'mysql':
            res=func(*args,**kwargs)  # 编写基于文件的认证,认证通过则执行,并返回res
    return wrapper

函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接收其他类别的参数,可以再deco的外部再包一层auth,用来专门接收额外的参数,这样便保证了再auth函数内无论有多少层都可以引用到

def auth(driver):
    def deco(func):
        ...
    return deco

此时我们就实现了一个有参装饰器,使用方式如下

# 先调用auth_type(drive='file'),得到@deco,deco是一个闭包函数,包含了对外部作用域名字driver的引用,@driver的语义与无参装饰器一样
@auth(driver='file')
def index():
    pass


@auth(driver='mysql')
def home():
    pass

可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于装饰之后的函数,查看文档注释


上一篇:每日一练20210501


下一篇:JavaScript中子函数访问外部变量的方法