一、知识点
装饰器是 Python 中实用的高阶函数特性,核心作用是:不修改原函数代码、不改变原函数调用方式的前提下,给函数增加额外功能。
1. 为什么需要装饰器
若多个函数需要添加同一类通用功能(如计时、日志),不用装饰器会导致重复代码(每个函数都要写一遍功能逻辑);装饰器可将通用功能抽离出来,一次定义、多处复用,原函数无需任何改动。
2. 装饰器的核心结构
装饰器本质是 "接收函数作为参数,返回新函数" 的高阶函数,核心分三层逻辑:
- 装饰器函数:接收被装饰的原函数作为参数;
- 嵌套的 wrapper(包装)函数:核心层,负责实现额外功能(如计时、日志),同时调用原函数,通过 * args/**kwargs 兼容原函数的任意参数,且保留原函数的返回值;
- 返回 wrapper 函数:用新的包装函数替换原函数,@装饰器名是简化写法(语法糖),等价于 "原函数 = 装饰器名 (原函数)"。
3. 核心执行流程(以计时装饰器为例)
调用装饰后的函数→实际执行的是 wrapper 函数→wrapper 先执行额外功能(如计时开始)→调用原函数执行核心逻辑→原函数执行完毕后,wrapper 再执行剩余额外功能(如计算并打印耗时)→最后返回原函数的返回值,保证原函数功能不受影响。
4. 关键细节
- *args/**kwargs:确保装饰器能适配有参数、无参数、多参数等任意形式的原函数,提升通用性;
- functools.wraps:可选优化,用于保留原函数的名称、注释等身份信息,避免装饰后函数 "身份" 变成 wrapper。
5. 常见应用场景
除了计时、日志,还可用于权限校验(如接口登录检查)、缓存(避免重复计算)、异常捕获(统一处理函数异常)、性能监控(统计调用次数 / 耗时)等。
核心总结
装饰器就像给函数 "穿外套":外套(wrapper)负责添加额外功能,里面的原函数本身不变,调用方式也和原来一样,却能多出来外套的功能。
二、普通函数与装饰器函数对比
1. 普通的函数
下面这个函数实现的是计算2到9999的所有质数(在大于 1 的自然数中,除了 1 和它自身外,不能被其他自然数整除的数),并且打印找到这些数需要的时间
-
- 定义一个判断是否为质数
-
- 定义一个函数,循环2到9999的数,通过判断质数函数来筛选每个数
-
- 在函数中通过time模块进行记时
会发现,这个time模块让整个代码逻辑很混乱,因为函数的主体是找质数,time模块是找质数的时间,如果可以time模块放在函数外,这样逻辑才清晰。
python
import time
def is_prime(num):
if num < 2:
return False
elif num == 2:
return True
else:
for i in range(2, num):
if num % i == 0:
return False
return True
def prime_nums():
t1 = time.time()
for i in range(2, 10000):
if is_prime(i):
print(i)
t2 = time.time()
print(f"执行时间:{t2 - t1}秒")
prime_nums()
2. 装饰器函数
python
import time
# 定义一个装饰器
def display_time(func):
def wrapper(): # 定义一个内部函数,在装饰器中wrapper函数是一个常用的函数名,并非强制,约定俗成的
start_time = time.time()
func() # 直接调用原函数(无参数),这里的func()是指装饰器需要修饰的函数,在这里是prime_nums()
end_time = time.time()
print(f"执行时间: {end_time - start_time} 秒")
return wrapper # return wrapper是返回函数对象,如果是return wrapper()则是立即执行wrapper函数
# 继续定义判断质数的函数
def is_prime(num):
"""
判断一个数是否为素数
"""
if num < 2:
return False
elif 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行是一个整体
"""
找出2到10000之间的所有素数并打印
"""
for i in range(2, 10000):
if is_prime(i):
print(i)
prime_nums()
# 执行时间每次都会变,但是变动不大,一般计算稳定的执行时间我们都是重复1000遍,然后取平均
装饰器的执行流程为:
- 定义装饰器函数 display_time:它接收一个函数 func 作为参数,并返回 wrapper 函数。
- 定义被装饰函数 prime_nums:此时 prime_nums 是一个普通函数对象。
- 应用装饰器:Python 自动将 prime_nums 作为参数传递给 display_time,即执行 display_time (prime_nums)。
- 替换原函数:display_time 返回 wrapper 函数,Python 用这个新函数覆盖了原来的 prime_nums。
也就是说装饰后,原函数名指向 wrapper,而非原始函数。
当你调用 prime_nums () 时,实际上执行的是 wrapper (),它会:
- 记录开始时间
- 调用 func ()(即原函数)
- 记录结束时间并打印耗时
这种等价的设计,会让初学者搞不懂为什么突然可以采取这种优雅的写法,类似的写法还有很多,在 python 中叫做语法糖:通过规范的写法来让代码更加优美和简洁,比如列表推导式也是,我们在后面再提。
你可以把 @理解为语法糖操作,实际上并非是 @装饰器,而是 @装饰器 + 下一行的代码 二者是一个整体。
3. 进一步拓展装饰器实现复用
可以看到,上述这个写法的时候,prime_nums()没有传入参数,如果函数有参数,那么必须给外部函数传入参数,也就是需要给外部的装饰器函数传入参数。
那么装饰器函数是需要复用的,不同的内部函数传入的参数不同,那就需要装饰器可以传入可变参数来维持这个特性。这就是说到了我们昨天的可变参数。
装饰器函数返回的是wrapper函数,所以,在调用装饰器函数的时候,返回的还是wrapper函数,而不是被修饰的函数。他是被修饰函数的外层函数,参数要大于等于被修饰函数的参数。
python
import time
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(3, 5) # 正常接收参数并计算
三、作业

python
def logger(func):
def wrapper(*args, **kwargs):
print(f"开始执行函数{func.__name__}, 参数:{args}, {kwargs}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕. 返回值:{result}")
return result
return wrapper
@logger
def multiply(a, b):
return a * b
multiply(2, 3)
html
开始执行函数multiply, 参数:(2, 3), {}
函数 multiply 执行完毕. 返回值:6
勇闯python的第30天@浙大疏锦行
一些碎碎念念:
转眼间,跟着疏老师系统学习Python已整整30天。回首这段时光,收获满满,心中满是感激------由衷感谢您提供了如此优质的学习机会,让我得以深入探索这门语言的魅力,您辛苦了!
本科期间,我并非计算机专业,但曾接触过SPSS、R语言等相关知识,当时便对数据与编程的世界充满好奇。而这次系统学习Python,更让我感受到了不一样的惊喜:从未想过,一门编程语言竟能将数字的严谨与视觉的美感完美融合,绘制出如此多优美的图案。这种"用代码创造美"的体验,让我对编程的兴趣愈发浓厚,也彻底打破了我对"编程枯燥"的刻板印象。 经过这30天的系统沉淀,我不仅加深了对Python的理解,更找到了持续探索的动力。
计算机语言的世界奥秘无穷,我早已满怀期待,渴望在接下来的学习中解锁更多技能,进一步探索其中的精彩。未来,我也会带着这份热爱,在编程的道路上稳步前行,不负这段宝贵的学习时光。