1. 装饰器的原则 -> 开放封闭原则

2. 装饰器的作用 -> 在不改变函数调用方式的基础上在函数的前、后添加功能

3. 引子

作为一个会写函数的python开发,我们从今天开始要去公司上班了。写了一个函数,就交给其他开发用了。
def fun():
    print('已经写好的函数')

季度末,公司的领导要给大家发绩效奖金了,就提议对这段日子所有人开发的成果进行审核,审核的标准是什么呢?就是统计每个函数的执行时间。
这个时候你要怎么做呀?
你一想,这好办,把函数一改:

import time


def fun():
    s_time = time.time()
    time.sleep(0.01)
    print('已经写好的函数')
    e_time = time.time()
    print('代码运行时间:{}'.format(e_time - s_time))


fun()

来公司半年,写了2000+函数,挨个改一遍,1个礼拜过去了,等领导审核完,再挨个给删了。。。又1个礼拜过去了。。。这是不是很闹心?
你觉得不行,不能让自己费劲儿,告诉所有开发,现在你们都在自己原本的代码上加上一句计算时间的语句?

import time


def fun():
    print('已经写好的函数')


s_time = time.time()
time.sleep(0.01)
fun()
e_time = time.time()
print('代码运行时间:{}'.format(e_time - s_time))

还是不行,因为这样对于开发同事来讲实在是太麻烦了。
那怎么办呢?你灵机一动,写了一个timer函数。。。

import time


def timer(fun):
    s_time = time.time()
    time.sleep(0.01)
    fun()
    e_time = time.time()
    print('代码运行时间:{}'.format(e_time - s_time))


def fun():
    print('已经写好的函数')


def fun2():
    print('已经写好的函数2')


timer(fun)
timer(fun2)

这样看起来是不是简单多啦?不管我们写了多少个函数都可以调用这个计时函数来计算函数的执行时间了。。。尽管现在修改成本已经变得很小很小了,但是对于同事来说还是改变了这个函数的调用方式,假如某同事因为相信你,在他的代码里用你的方法用了2w多次,那他修改完代码你们友谊的小船也就彻底地翻了。
你要做的就是,让你的同事依然调用func1,但是能实现调用timer方法的效果。

import time


def timer(fun):
    s_time = time.time()
    time.sleep(0.01)
    fun()
    e_time = time.time()
    print('代码运行时间:{}'.format(e_time - s_time))


def fun():
    print('已经写好的函数')


fun = timer  # 要是能这样的就完美了。。。可惜报错
fun()

非常可惜,上面这段代码是会报错的,因为timer方法需要传递一个func参数,我们不能在赋值的时候传参,因为只要执行func1 = timer(func1),timer方法就直接执行了,下面的那句func1根本就没有意义。到这里,我们的思路好像陷入了僵局。。。

4. 装饰器的形成

# 装饰器简单版本1 -> 通过闭包实现

import time


def timer(fun):
    def inner():
        s_time = time.time()
        time.sleep(0.01)
        fun()
        e_time = time.time()
        print('代码运行时间:{}'.format(e_time - s_time))

    return inner


def fun():
    print('已经写好的函数')


fun = timer(fun) # 实际上是调用了 inner 函数
fun()

忙活了这么半天,终于初具规模了!现在已经基本上完美了,唯一碍眼的那句话就是还要在做一次赋值调用。。。
你觉得碍眼,python的开发者也觉得碍眼,所以就为我们提供了一句语法糖来解决这个问题!

# 装饰器 -> 语法糖

import time


def timer(fun):
    def inner():
        s_time = time.time()
        time.sleep(0.01)
        fun()
        e_time = time.time()
        print('代码运行时间:{}'.format(e_time - s_time))

    return inner


@timer  # 语法糖: @装饰器函数名 -> 等价于 fun = timer(fun)
def fun():
    print('已经写好的函数')


fun()


到这里,我们可以简单的总结一下:
  • 装饰器的本质:一个闭包函数
  • 装饰器的功能:在不修改原函数及其调用方式的情况下对原函数功能进行扩展
还有最后一个问题要解决,刚刚我们讨论的装饰器都是装饰不带参数的函数,现在要装饰一个带参数的函数怎么办呢?

# 装饰器 -> 传递一个参数

import time


def timer(fun):
    def inner(s):
        s_time = time.time()
        time.sleep(0.01)
        fun(s)
        e_time = time.time()
        print('代码运行时间:{}'.format(e_time - s_time))

    return inner


@timer
def fun(s):
    print(s)


fun('已经写好的函数')

其实装饰带参的函数并不是什么难事,但假如你有两个函数,需要传递的参数不一样呢?

# 装饰器 -> 传递不同参数 -> 解决办法 *args **kwargs

import time


def timer(fun):
    def inner(*args, **kwargs):
        s_time = time.time()
        time.sleep(0.01)
        fun(*args, **kwargs)
        e_time = time.time()
        print('代码运行时间:{}'.format(e_time - s_time))

    return inner


@timer
def fun(a, b, c):
    print(a, b, c)


@timer
def fun2(x, y):
    print(x, y)


fun('fun参数1', 'fun参数2', 'fun参数3')
fun2('fun2X', 'funY')

现在参数的问题已经完美的解决了,可是如果你的函数是有返回值的呢?

import time


def timer(fun):
    def inner(*args, **kwargs):
        s_time = time.time()
        time.sleep(0.01)
        re = fun(*args, **kwargs)
        e_time = time.time()
        print('代码运行时间:{}'.format(e_time - s_time))
        return re

    return inner


@timer
def fun(a):
    print(a)
    return '这是返回值'


return_val = fun('fun参数1')
print(return_val)

如果该装饰器被100个函数使用,那么有一天突然说这100个函数不用这个装饰器了,那么就要把 @xxx 删除100遍,我们可以通过 装饰器传参 解决这个问题 -> 详细看第6点

import time

flag = False


def timer_out(flag):
    def timer(fun):
        def inner(*args, **kwargs):
            if flag:
                s_time = time.time()
                time.sleep(0.01)
                re = fun(*args, **kwargs)
                e_time = time.time()
                print('代码运行时间:{}'.format(e_time - s_time))
                return re
            else:
                re = fun(*args, **kwargs)
                return re

        return inner

    return timer


@timer_out(flag)
def fun(a):
    print(a)
    return '这是返回值'


return_val = fun('fun参数1')
print(return_val)

5. 装饰器的固定模式

def wrapper(fn):  # 装饰器函数,fn是被装饰的函数
    def inner(*args, **kwargs):
 # 在被装饰函数之前要做的事
        re = fn(*args, **kwargs)  # 被装饰的函数
# 在被装饰函数之后要做的事
        return re

    return inner


@wrapper
def be_wrapper(data):
    print(data)
    return '返回值'


return_val = be_wrapper('参数1')
print(return_val)

6. 带参数的装饰器

  • @wrapper_out('参数') -> @ 和 wrapper_out('参数') 拆开来看就明白了,wrapper_out() 返回的是 wrapper 函数,然后再将 wrapper 给 @

flag = False


def wrapper_out(flag):
    def wrapper(fn):
        def inner(*args, **kwargs):
            print(flag)

            if flag:
                print('扩展的代码一')
                re = fn(*args, **kwargs)
                print('扩展的代码二')
                return re
            else:
                re = fn(*args, **kwargs)
                return re

        return inner

    return wrapper


'''
    wrapper = wrapper_out(flag)
    be_wrapper = wrapper(be_wrapper)
    等价于
    @wrapper_out(flag) 将 @ 和 wrapper_out(flag) 拆开看就明白了
'''


@wrapper_out(flag)
def be_wrapper():
    print('被装饰的函数')


be_wrapper()

7. 多个装饰器装饰1个函数 -> 先记现象再理解步骤

def wrapper1(func):
    def inner1():
        print('wrapper1 ,before func')
        ret = func()
        print('wrapper1 ,after func')
        return ret

    return inner1


def wrapper2(func):
    def inner2():
        print('wrapper2 ,before func')
        ret = func()
        print('wrapper2 ,after func')
        return ret

    return inner2

@wrapper2
@wrapper1
def f():
    print('in f')
    return '哈哈哈'

print(f()) # 哈哈哈

  • 现象

wrapper2 ,before func
wrapper1 ,before func
in f
wrapper1 ,after func
wrapper2 ,after func

  • 步骤 -> 装饰器会先执行离函数最近的装饰器


8. wraps 装饰器修复技术

  • 当函数使用了装饰器,我们在调用该函数的时候实际上是装饰器里的函数,此时我们想获取该函数的基本信息的时候实际上是获取了装饰器里面函数的基本信息,可以通过 wraps 将装饰器里的函数基本信息指向被装饰的函数上

  • 在以后编写装饰器的时候最好使用上 wraps,以免报错

  • 没加 wraps 获取函数的基本信息

def wrapper(fn):
    def inner(*args, **kwargs):
        re = fn(*args, **kwargs)
        return re

    return inner


@wrapper
def be_wrapper():
    print('被装饰的函数')


fn_name = be_wrapper.__name__  # 函数名:inner

  • 加了 wraps 获取函数的基本信息

from functools import wraps


def wrapper(fn):
    @wraps(fn)
    def inner(*args, **kwargs):
        rt = fn(*args, **kwargs)
        return rt

    return inner


@wrapper
def be_wrapper():
    print('被装饰的函数')


fn_name = be_wrapper.__name__ # 函数名:be_wrapper

9. 作业

  • 编写装饰器:在多个函数在执行之前验证是否登录了,如果登录了后面的函数执行就不需要验证

flag = False


def wrapper(fn):
    def inner(*args, **kwargs):
        global flag
        if flag:
            re = fn()
            return re
        else:
            username = input('账号:')
            password = input('密码:')
            if username == 'Kevin' and password == '123':
                flag = True
                re = fn()
                return re
            else:
                print('登录失败')
    return inner

@wrapper
def features():
    print(1)


@wrapper
def features2():
    print(2)


features()
features2()

  • 编写装饰器:编写装饰器,为多个函数加上记录调用功能,要求每次调用函数都将被调用的函数名称写入文件

def log(fn):
    def inner():

        with open('log.txt', 'a', encoding='utf-8') as f:
            f.write(fn.__name__ + '\n')

    return inner


@log
def log_fn1():
    print(1)


@log
def log_fn2():
    print(1)


log_fn1()
log_fn2()

  • 编写下载网页内容的函数,要求功能是:用户传入一个url,函数返回下载页面的结果
  • 为题目1编写装饰器,实现缓存网页内容的功能:
  • 具体:实现下载的页面存放于文件中,如果文件内有值(文件大小不为0),就优先从文件中读取网页内容,否则,就去下载,然后存到文件中

import os
from urllib.request import urlopen


def save_html(fn):
    def inner(*args, **kwargs):
        code = fn(*args, **kwargs)

        if os.path.getsize('save_html.txt'):
            with open('save_html.txt', 'rb') as f:
                print('读取文件中内容')
                return f.read()
        with open('save_html.txt', 'wb') as f:
            f.write(code)
            print('新增网页内容到文件中')
            return code

    return inner


@save_html
def get_html(url):
    code = urlopen(url).read()
    return code


print(get_html('http://www.baidu.com'))
print(get_html('http://www.baidu.com'))