为什么要使用装饰器

主要是为了符合软件设计的原则,对修改封闭,对扩展开放,简单来说,就是软件要具有扩展性,要方便新增功能,但是又不能涉及到大量地修改源码来适应需求,否则如果关键对象被修改,很可能牵一发而动全身,整个项目可能会推翻重来,代价太高

有一些函数,在项目中很关键,不希望它被修改,但是又要顾及增加新功能,这个时候如果不用装饰器,就会很困扰

什么是装饰器

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

装饰器的实现

无参装饰器

如果想为下面的函数添加统计时间功能

import time


def index():
    time.sleep(3)
    print('welcome')
    return 200


index()  # 函数执行

在不修改原函数的情况下,最容易想到的是这样的做法

import time


def index():
    time.sleep(3)
    print('welcome')
    return 200


start = time.time()
index()  # 函数执行
end = time.time()
print('运行时间:%s' % (end - start))

我们会想到,统计执行时间的功能还可以进一步封装成一个函数

import time


def index():
    time.sleep(3)
    print('welcome')
    return 200


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(index)这种方式调用index实在是太不爽了,于是我们想到了闭包,将要执行的函数包给统计时间功能的做法

import time


def index():
    time.sleep(3)
    print('welcome')
    return 200


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


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

# 通过把index传递给timer函数,之后的返回值覆盖全局名称空间的index
# 这种方式处理之后,index函数的调用方式没有发生改变
# 拿到代码写程序的人对这件事情毫无感觉,但是其实程序新增了一些功能
index = timer(index)
index()

以上方法使用于需要修饰的函数是一个无参函数,如果是一个有参函数,则程序会抛出异常

import time


def index():
    time.sleep(3)
    print('welcome')
    return 200


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


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


def home(name):
    time.sleep(5)
    print('Welcome to the home page', name)


home = timer(home)  # home = wrapper
home('egon')		# wrapper('egon')但是wrapper是一个无参函数

只需要做一些修改

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

这样就基本实现了一个装饰器了,但是装饰之后的函数调用起来还是有点麻烦,需要重复下面两步

home = timer(home)
home('egon')

python为装饰器的使用专门准备了语法糖,只需要在需要装饰的函数头顶上加上@装饰器函数名即可

@timer  # 相当于做了home = timer(home)
def home(name):
    time.sleep(5)
    print('Welcome to the home page', name)


home('alex')

可以为函数添加多个装饰器

@timer  # 相当于做了home = timer(home)
@f1
@f2
def home(name):   # home = timer(f1(f2(home)))
    time.sleep(5)
    print('Welcome to the home page', name)


home('alex')

有参装饰器

如果我们的情况更加复杂,需要在之前定义的wrapper函数中添加验证功能,验证功能产生多种结果,根据这些结果执行不同的代码,简单的做法如下

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

    return wrapper

wrapper函数需要一个driver变量,但是wrapper函数和deco函数都有其特定功能,不能再接受额外的参数,只能再套一层,做成有参装饰器

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

        return wrapper
    return deco

调用有参装饰器

@auth(driver='aaaa')	# -> @deco
def index():
    time.sleep(3)
    print('welcome')
    return 200

完美的装饰器

通过help(函数名)的方式可以获取函数的帮助文档

print(help(index))
# Help on function wrapper in module __main__:
# 
# wrapper(*args, **kwargs)
# 
# None

但是此时通过help打印出来的却是wrapper函数的帮助文档,我们想让这个帮助文档是index函数的,python的模块专门为装饰器提供了修饰的函数

from functools import wraps

def timer(func):
    @wraps(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