知识点回顾:
1. 装饰器的思想:进一步复用
2. 函数的装饰器写法
3. 注意内部函数的返回值
昨天我们接触到了函数大部分的功能,然后在你日常 ctrl 点进某个复杂的项目,发现函数上方有一个@xxx,它就是装饰器。
装饰器本质上是一个 Python 函数,它可以让其他函数或方法在不需要做任何代码修改的前提下增加额外功能。--本质是如果让一个函数具备太多功能,那么他看起来就会比较乱,可读性比较差,如果把其中一部分相同甚至可以复用的功能用一个新的函数来调用,然后让2个函数同时实现,就会做到:
- 进一步封装了函数的一些用法,做到dry原则(don't repeat yourself)
- 使函数更加具有可读性
一、普通的函数
下面这个函数实现的是计算2到9999的所有质数(在大于 1 的自然数中,除了 1 和它自身外,不能被其他自然数整除的数),并且打印找到这些数需要的时间。
- 定义一个判断是否为质数
- 定义一个函数,循环2到9999的数,通过判断质数函数来筛选每个数
- 在函数中通过time模块进行记时
会发现,这个time模块让整个代码逻辑很混乱,因为函数的主体是找质数,time模块是找质数的时间,如果可以time模块放在函数外,这样逻辑才清晰。
import time
def is_prime(num):
if num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True # 所有数求余完之后都没有余数,才是质数
def prime_nums():
n = 0
t1 = time.time()
for i in range(2, 10000):
if is_prime(i):
n += 1
t2 = time.time()
print(f"判断2-9999是否为质数,执行时间:{t2 - t1},质数个数:{n}。")
prime_nums()
输出:
判断2-9999是否为质数,执行时间:0.19123458862304688,质数个数:1229。
二、装饰器函数
import time
# 定义一个装饰器
def display_time(func):
def wrapper(): # 定义一个内部函数,在装饰器中wrapper函数是一个常用的函数名
t1 = time.time()
func() # 直接调用原函数(无参数),这里的func()是指装饰器需要修饰的函数,这里是prime_nums()
t2 = time.time()
print(f"执行时间:{t2 - t1}秒。")
return wrapper # 是返回函数对象,如果是return wrapper()则是立即执行wrapper函数
装饰器的本质是一个高阶函数,它接收一个函数作为参数,并返回一个新函数来替代原函数。这个新函数需要:
- 保留原函数的调用方式(参数和返回值)。
- 在原函数执行前后添加额外逻辑(如计时、日志等)。
因此,我们需要在装饰器内部定义一个新函数来实现这些功能。
# 继续定义判断质数的函数
def is_prime(num):
"""
判断一个数num是否为质数
"""
if num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True # 所有数求余完之后都没有余数,才是质数
# 装饰器的标准写法
@display_time
def prime_nums(): # 这两行是一个整体
"""
找出2-9999中的所有质数
"""
for i in range(2, 10000):
if is_prime(i):
print(i)
prime_nums()
# 执行时间每次都会变,但是变动不大,一般计算稳定的执行时间我们都是重复1000遍,然后取平均
输出:略
三、进一步拓展装饰器实现复用
可以看到,上述这个写法的时候,prime_nums()没有传入参数,如果函数有参数,那么必须给外部函数传入参数,也就是需要给外部的装饰器函数传入参数。
那么装饰器函数是需要复用的,不同的内部函数传入的参数不同,那就需要装饰器可以传入可变参数来维持这个特性。这就是说到了我们昨天的可变参数
装饰器函数返回的是 wrapper 函数,所以,在调用装饰器函数的时候,返回的还是wrapper函数,而不是被修饰的函数。他是被修饰函数的外层函数,参数要大于等于被修饰函数的参数。
import time
from unittest import result
def display_time(func):
"""支持任意参数的时间统计装饰器"""
def wrapper(*args, **kwargs): # 接受任意数量的位置参数和关键字参数
t1 = time.time()
result = func(*args, **kwargs) # 将参数传递给原函数
t2 = time.time()
print(f"执行时间: {t2 - t1} 秒")
return result # 返回原函数的返回值
return wrapper
@display_time
def add(a, b):
return a + b
add(1, 5)
输出:
执行时间: 0.0 秒
6
最后一个tips:注意下内部函数有无返回值?
注意到之前被修饰的函数在无参数情况下,wrapper里面只有 func() ,现在是result = func(*args, **kwargs) 以及加上了 return result
为什么会这样?因为被修饰的函数是return xxxx,而不是print xxx,被修饰的函数如果有返回值,装饰器函数就需要搭配返回值。
四、今日作业
编写一个装饰器 logger,在函数执行前后打印日志信息(如函数名、参数、返回值)
示例:
@logger
def multiply(a, b):
return a * b
multiply(2, 3)
# 输出:
# 开始执行函数 multiply,参数: (2, 3), {}
# 函数 multiply 执行完毕,返回值: 6
答案:
def logger(func): # 装饰器
"""在func执行前后打印日志信息"""
def wrapper(*args, **kwargs):
print(f"开始执行函数 {func.__name__},参数:{args}, {kwargs}")
result=func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕,返回值:{result}")
return wrapper
@logger
def multiply(a, b):
return a * b
multiply(3, 4) # 两个位置参数
输出:
开始执行函数 multiply,参数:(3, 4), {}
函数 multiply 执行完毕,返回值:12
传入参数:一个位置参数(在前),一个关键字参数(在后)
multiply(3, b=4) # 一个位置参数(在前),一个关键字参数(在后)
输出:
开始执行函数 multiply,参数:(3,), {'b': 4}
函数 multiply 执行完毕,返回值:12
传入参数:两个关键字参数
multiply(a=3, b=4) # 两个关键字参数
输出:
开始执行函数 multiply,参数:(), {'a': 3, 'b': 4}
函数 multiply 执行完毕,返回值:12