Python 深入浅出装饰器
本篇文章将详细介绍装饰器的来龙去脉!
参考文章:Python 装饰器 | 简单一点学习 easyeasy.me
1. 装饰器是啥?
装饰器(Decorator)本质上是一个函数(或类),它接受一个函数作为输入,然后返回一个新的函数。新的函数通常会在原函数的基础上增加一些功能,比如记录日志、计时、权限验证等。
用生活化的比喻,装饰器就像是给你的手机壳加个防摔功能。手机(原函数)还是那个手机,但壳(装饰器)让它多了点保护功能。你不用改手机本身,就能让它更耐用。
在 Python 中,装饰器通常用 @
符号来写,比如:
@my_decorator
def my_function():print("我是一个函数!")
这里的 @my_decorator
就是在 my_function
上加了个装饰器。接下来我们一步步揭开它的神秘面纱。
2. 函数的基础知识
在讲装饰器之前,得先搞清楚 Python 函数的一些基本概念,因为装饰器本质上是对函数的操作。
2.1 函数是“第一等公民”
在 Python 中,函数可以:
- 赋值给变量
- 作为参数传递给其他函数
- 作为返回值从函数返回
来看个例子:
def say_hello():print("Hello!")# 赋值给变量
greet = say_hello
托管
greet() # 输出:Hello!# 作为参数传递
def call_func(func):func()call_func(say_hello) # 输出:Hello!
这说明函数可以像普通变量一样被操作,为装饰器提供了基础。
2.2 闭包和内部函数
装饰器经常用到闭包(closure),也就是函数里定义的函数能记住外部函数的变量。简单例子:
def outer():x = 10def inner():print(x) # 能访问外层的 xreturn innerfunc = outer()
func() # 输出:10
这个特性让装饰器可以“包装”函数并保留额外的信息。
3. 从简单例子入手
让我们直接写一个简单的装饰器,感受一下它的威力。
3.1 例子:记录函数执行时间
假设我们想知道一个函数跑了多久,可以用装饰器来实现:
import timedef timer(func):def wrapper():start = time.time()func()end = time.time()print(f"{func.__name__} 运行时间:{end - start} 秒")return wrapper@timer
def slow_function():time.sleep(2)print("我睡了 2 秒")slow_function()
# 输出:
# 我睡了 2 秒
# slow_function 运行时间:2.002345 秒
3.2 发生了什么?
@timer
等价于slow_function = timer(slow_function)
。timer
函数接收slow_function
作为参数,返回一个新的函数wrapper
。wrapper
函数在调用func()
(即slow_function
)前后加了计时代码。
这个例子展示了装饰器的基本逻辑:用一个函数包装另一个函数,增加新功能。
4. 装饰器的工作原理
为了更深入理解,我们来拆解一下装饰器的结构。
4.1 装饰器的本质
装饰器是一个高阶函数,接收一个函数,返回一个新的函数。新函数通常会调用原函数,并在前后加点“料”。
上面的 timer
装饰器可以改写成这样(去掉 @
语法糖):
def slow_function():time.sleep(2)print("我睡了 2 秒")slow_function = timer(slow_function)
slow_function()
4.2 wrapper 函数的参数问题
上面的例子有个问题:如果 slow_function
有参数怎么办?我们需要让 wrapper
函数能接受任意参数。
改进版:
def timer(func):def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print(f"{func.__name__} 运行时间:{end - start} 秒")return resultreturn wrapper@timer
def greet(name):time.sleep(1)print(f"你好,{name}!")return f"问候 {name}"result = greet("小明")
# 输出:
# 你好,小明!
# greet 运行时间:1.00123 秒
# result = "问候 小明"
这里用了 *args
和 **kwargs
来处理任意参数和关键字参数,还返回了原函数的返回值。
5. 带参数的装饰器
有时候,我们希望装饰器本身也能接受参数。比如,控制日志的级别:
def log(level):def decorator(func):def wrapper(*args, **kwargs):print(f"[{level}] {func.__name__} 被调用")return func(*args, **kwargs)return wrapperreturn decorator@log("INFO")
def say_hello(name):print(f"你好,{name}!")say_hello("小明")
# 输出:
# [INFO] say_hello 被调用
# 你好,小明!
5.1 原理拆解
带参数的装饰器多了一层函数嵌套:
log(level)
返回一个装饰器函数decorator
。decorator
再接收目标函数func
,返回wrapper
。@log("INFO")
等价于say_hello = log("INFO")(say_hello)
。
这让装饰器变得更灵活,可以根据参数定制行为。
6. 装饰器的实际应用场景
装饰器在实际开发中超级实用,以下是一些常见的场景。
6.1 日志记录
像上面的 log
装饰器,可以记录函数的调用信息:
def log(func):def wrapper(*args, **kwargs):print(f"调用 {func.__name__},参数:{args}, {kwargs}")return func(*args, **kwargs)return wrapper
6.2 权限验证
比如,只有管理员才能调用某些函数:
def require_admin(func):def wrapper(user, *args, **kwargs):if user.get("role") == "admin":return func(user, *args, **kwargs)else:raise PermissionError("需要管理员权限")return wrapper@require_admin
def delete_user(user, user_id):print(f"用户 {user_id} 已被删除")user = {"role": "admin"}
delete_user(user, 123) # 正常执行
user = {"role": "guest"}
# delete_user(user, 123) # 抛出 PermissionError
6.3 缓存
可以用装饰器实现函数结果缓存,提高性能:
def cache(func):cache_dict = {}def wrapper(*args):if args in cache_dict:return cache_dict[args]result = func(*args)cache_dict[args] = resultreturn resultreturn wrapper@cache
def fib(n):if n <= 1:return nreturn fib(n-1) + fib(n-2)print(fib(10)) # 计算很快,因为中间结果被缓存
7. 高级用法:类装饰器和多重装饰器
7.1 类装饰器
装饰器不一定是函数,也可以用类来实现:
class Timer:def __init__(self, func):self.func = funcdef __call__(self, *args, **kwargs):start = time.time()result = self.func(*args, **kwargs)end = time.time()print(f"{self.func.__name__} 运行时间:{end - start} 秒")return result@Timer
def slow_function():time.sleep(2)print("我睡了 2 秒")slow_function()
类装饰器通过 __call__
方法让对象可以像函数一样被调用,适合需要保存状态的场景。
7.2 多重装饰器
可以给一个函数加多个装饰器:
@log("INFO")
@timer
def greet(name):time.sleep(1)print(f"你好,{name}!")greet("小明")
# 输出:
# [INFO] greet 被调用
# 你好,小明!
# greet 运行时间:1.00123 秒
多重装饰器的执行顺序是从内到外(由下到上),但定义顺序相反。比如,log
先执行,然后是 timer
。
8. 常见坑和怎么避坑
8.1 函数元信息丢失
装饰器可能会覆盖原函数的元信息(比如 __name__
和 __doc__
)。可以用 functools.wraps
解决:
from functools import wrapsdef timer(func):@wraps(func)def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs)end = time.time()print(f"{func.__name__} 运行时间:{end - start} 秒")return resultreturn wrapper@timer
def greet(name):"""问候用户"""print(f"你好,{name}!")print(greet.__name__) # 输出:greet
print(greet.__doc__) # 输出:问候用户
@wraps(func)
会保留原函数的元信息。
8.2 性能问题
装饰器会增加函数调用的开销,尤其是在嵌套多层装饰器时。尽量保持 wrapper
函数简单,避免复杂逻辑。
8.3 调试困难
装饰器可能会让代码逻辑变得复杂,调试时可以用 print
或日志记录 wrapper
的执行过程。
9. 总结与实践建议
装饰器是 Python 中一个强大且灵活的工具,能让你的代码更简洁、更模块化。
- 装饰器的本质是函数(或类)包装,增加功能。
- 简单装饰器用
@
语法糖,带参数的装饰器多一层嵌套。 - 实际应用场景包括日志、权限、缓存等。
- 注意元信息丢失问题,用
functools.wraps
解决。