Skip to content

装饰器(Decorator)

Python 里的装饰器 (decorator),它不难,但却几乎是 “精通” Python 的路上的第一道关卡。让我们来看看它到底是什么东西,为什么我们需要它。

把函数当参数使用

有以下两个函数,突然有一个需求,需要计算两个函数的运行时间,请问可以怎么做 ?

python
import requests
import time


def get_html(url):
    response = requests.get(url)
    return response.text


def save_html(html, name):
    with open(name, mode='w', encoding='utf-8') as f:
        f.write(html)


html = get_html('http://www.baidu.com')
save_html(html, 'baidu')

第一次封装

python
# 在外部添加代码实现效果
start = time.time()
html = get_html('http://www.baidu.com')
print(time.time() - start)
start = time.time()
save_html(html, 'baidu')
print(time.time() - start)

"""
    直接在函数外部添加计算时间的代码(需求代码)
        优点:简单,哪里需要就加哪里
        缺点:可能需要的地方会很多
"""
python
# 直接修改源码

def get_html(url):
    start_time = time.time()
    response = requests.get(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return response.text


def save_html(html, name):
    start_time = time.time()
    with open(name, mode='w', encoding='utf-8') as f:
        f.write(html)
    print('保存页面需要的时间:', time.time() - start_time)


html = get_html('http://www.baidu.com')
save_html(html, 'baidu.html')

"""
    直接在原来的代码上进行修改
        优点:修改的地方会很小
        缺点:所有的调用的地方都会修改(入侵性)
"""
python
# 封装成函数便于重复调用

def wrapper_get_html(url):
    start_time = time.time()
    html = get_html(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return html


def wrapper_save_html(html, name):
    start_time = time.time()
    save_html(html, name)
    print('保存页面需要的时间:', time.time() - start_time)


html = wrapper_get_html('http://www.baidu.com')
wrapper_save_html(html, 'baidu.html')

"""
    定义一个函数,专门用于计算时间
        优点:新增一个函数,对原有的代码没有入侵性
        缺点:当这个功能很多地方都需要调用时,需要封装非常多个函数
"""

第二次封装

第一次封装的方案三看起来还可以,实际上使用的时候还是有很多的补足。例如原有的函数变成了四个,也就是下面的代码:

python
import requests
import time


def get_html(url):
    response = requests.get(url)
    return response.text


def get_baidu_html(url):
    response = requests.get(url)
    return response.text


def get_souhu_html(url):
    response = requests.get(url)
    return response.text


def save_html(html, name):
    with open(name, mode='w', encoding='utf-8') as f:
        f.write(html)


"""
    这三个函数的参数一样,返回值一样
"""

接下来继续封装

python
# 封装成函数便于重复调用
def wrapper_get_html(url):
    start_time = time.time()
    html = get_html(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return html


def wrapper_get_baidu_html(url):
    start_time = time.time()
    html = get_baidu_html(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return html


def wrapper_get_souhu_html(url):
    start_time = time.time()
    html = get_souhu_html(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return html


def wrapper_save_html(html, name):
    start_time = time.time()
    save_html(html, name)
    print('保存页面需要的时间:', time.time() - start_time)


# html = get_html('http://www.baidu.com')
# save_html(html, 'baidu.html')


# html = get_html('http://www.soso.com')
# save_html(html, 'soso.html')

html = wrapper_get_html('http://www.soso.com')
wrapper_save_html(html, 'baidu.html')

html = wrapper_get_html('http://www.baidu.com')
wrapper_save_html(html, 'soso.html')
"""
    定义一个函数,专门用于计算时间
        优点:新增一个函数,对原有的代码没有入侵性
        缺点:当这个功能很多地方都需要调用时,需要封装非常多个函数
"""
python
# 重复的逻辑抽取一个通用函数

def wrapper_func_get_html(func, url):
    # 封装所有的请求方法
    start_time = time.time()
    html = func(url)
    print('请求页面需要的时间:', time.time() - start_time)
    return html


def wrapper_func_save_html(func, html, name):
    start_time = time.time()
    ret = func(html, name)
    print('保存页面需要的时间:', time.time() - start_time)
    return ret

# 需要对哪一个函数计算时间,就把那一个函数传递进去嗲用
html = wrapper_func_get_html(get_html, 'http://www.baidu.com')
wrapper_func_save_html(html, 'baidu.html')

html = wrapper_func_get_html(get_baidu_html, 'http://www.baidu.com')
wrapper_func_save_html(html, 'baidu.html')

"""
    抽取一个通用的函数,专门用于计算时间
        优点:用一个函数可以实现对某一个函数的时间机选
        缺点:每次调用的时候需要传入函数对象
"""
python
# 抽取一个通用计算时间函数

def wrapper_func(func, *args, **kwargs):
    # 封装所有的请求方法
    start_time = time.time()
    ret = func(*args, **kwargs)
    print(time.time() - start_time)
    return ret


# html = get_html('http://www.baidu.com')
# save_html(html, 'baidu.html')

# html = get_html('http://www.soso.com')
# save_html(html, 'soso.html')

# 需要对哪一个函数计算时间,就把那一个函数传递进去用
html = wrapper_func(get_html, 'http://www.baidu.com')
wrapper_func(save_html, html, 'baidu.html')

html = wrapper_func(get_html, 'http://www.soso.com')
wrapper_func(save_html, html, 'soso.html')

"""
    抽取一个通用的函数,专门用于计算时间
        优点:用一个函数可以实现对某一个函数的时间计算
        缺点:每次调用的时候需要传入函数对象
"""

对于方案三来说,封装的其实已经非常好了。但是对于我们来说,还不是那么的好用。例如在代码中 get_htmlsave_html 就分别传递了两次,那么这个还可以继续优化吗 ?

装饰函数对象

闭包里面可以缓存变量,那么是否可以将之前重复传递的函数对象给缓存到闭包里面呢? 答案是可以的

python
import time

import requests


def save_html(html, name):
    with open(name, mode='w', encoding='utf-8') as f:
        f.write(html)


def get_html(url):
    response = requests.get(url)
    return response.text

def wrapper_func(func, *args, **kwargs):  // [!code --]
    start_time = time.time()  // [!code --]
    ret = func(*args, **kwargs)  // [!code --]
    print(time.time() - start_time)  // [!code --]
    return ret  // [!code --]

def timer(func):  // [!code ++]
    def warp(*args, **kwargs): // [!code ++]
        start_time = time.time() // [!code ++]
        result = func(*args, **kwargs) // [!code ++]
        print(time.time() - start_time) // [!code ++]
        return result // [!code ++]

    return warp


# 使用同样的名字,覆盖之前的函数效果
get_html = timer(get_html)
save_html = timer(save_html)

html = get_html('https://www.baidu.com')
save_html(html, 'baidu.html')

"""
    抽取一个通用的函数,专门用于计算时间
        优点:用一个函数可以实现对某一个函数的时间机选
        缺点:
"""

装饰器语法糖

前面的最后一次封装,其实就是装饰器最终的成品。但我们一般使用的时候,采用的是装饰器语法糖,也就是@ 符号。

python
import time

import requests

# 装饰器需要定义在函数前面
def timer(func): 
    def warp(*args, **kwargs):
        start_time = time.time() 
        result = func(*args, **kwargs) 
        print(time.time() - start_time)
        return result 

    return warp

@timer
def save_html(html, name): # 此处 @timer 等价于 get_html = timer(get_html)
    with open(name, mode='w', encoding='utf-8') as f:
        f.write(html)


@timer
def get_html(url): # 此处 @timer 等价于 save_html = timer(save_html) 
    response = requests.get(url)
    return response.text


get_html = timer(get_html) // [!code --]
save_html = timer(save_html) // [!code --]

html = get_html('https://www.baidu.com')
save_html(html, 'baidu.html')

这就是我们最常见的装饰器的形式了,这两种写法完全等价,只是 @ 写法更简洁一些。

案例:URL地址过滤

python
"""
    过滤下列一串网址中不属于猫眼的网址
"""
urls = [
    'https://maoyan.com/board/4?offset=0',
    'https://maoyan.com/board/4?offset=10',
    'https://maoyan.com/board/4?offset=20',
    'https://maoyan.com/board/4?offset=30',
    'https://www.baidu.com',
    'https://www.sohu.com',
    'https://maoyan.com/board/4?offset=40',
    'https://maoyan.com/board/4?offset=50',
]

import requests


def download_maoyan(url):
    response = requests.get(url)
    print(url)
    return response.text


for url in urls:
    download_maoyan(url=url)
参考答案
python
def filter_url(func):
    def wrapper(*args, **kwargs):
        if 'maoyan' in kwargs['url']:
            result = func(*args, **kwargs)
            print('下载成功', kwargs['url'])
            return result
        else:
            print('不是猫眼电影的网址', kwargs['url'])

    return wrapper

案例:过滤用户年龄

python
"""
定义一个用户模型(User)
    属性:姓名,年龄

定义一个方法:watch_movie。方法接收一个用户,
调用方法后打印 用户姓名、年龄、正在看电影
"""


class User:
    """用户模型"""

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name


def watch_movie(user=None):
    print("%s 正在观看电影" % user)


user1 = User('张三', 17)
user2 = User('李四', 18)

watch_movie(user=user1)
watch_movie(user=user2)
参考答案
python
def filter_age(func):
    def wrapper(*args, **kwargs):
        if kwargs['user'].age >= 18:
            return func(*args, **kwargs)
        else:
            # 主动抛出错误
            # Exception
            # raise Exception('未满十八岁不能观看')
            print(f"{kwargs['user']} 未满十八岁不能观看")

    return wrapper