装饰器(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_html
、save_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