Python 装饰器进阶:从入门到实战,写出更灵活的函数增强技巧
很多 Python 开发者都听过装饰器,也知道怎么写简单的装饰器。但大多数人对装饰器的进阶用法,比如带参数的装饰器、类装饰器、装饰器装饰类、保留原函数信息等技巧,往往理解不够深入。本文从基础出发,带你一步步掌握装饰器的进阶用法,并且通过三个实用案例帮助你理解装饰器的实际应用。
读完本文,你就能灵活运用装饰器,写出更加优雅的 Python 代码。 --- ## 一、装饰器基础回顾装饰器本质上是一个接受函数作为输入,并返回一个新函数的"函数"。它允许你在不修改原函数代码的情况下,给函数增加额外的功能。这是 Python 中一种"面向切面编程"的实现方式。
一个最简单的装饰器就是日志装饰器:
```python import functools def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"[LOG] 调用函数: {func.__name__}") result = func(*args, **kwargs) return result return wrapper @logger def add(a, b): return a + b print(add(1, 2)) ```这里用到了 functools.wraps,这很重要,它可以保留原函数的元信息(比如函数名、文档字符串),不然 add.__name__ 会变成 wrapper,这会影响调试。
很多时候我们需要给装饰器本身传递参数,比如日志装饰器,我们需要指定不同的日志级别,或者不同的前缀,这种情况下我们就需要套一层函数来接收参数。
原理其实很简单:带参数的装饰器,就是 外层函数接收参数,然后返回一个真正的装饰器,然后这个装饰器才去装饰原函数。
比如我们实现一个带前缀配置的日志装饰器: ```python import functools def logger(prefix="[LOG]"): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"{prefix} 调用函数: {func.__name__}") return func(*args, **kwargs) return wrapper return decorator # 使用的时候就要加括号传递参数 @logger(prefix="[INFO]") def multiply(a, b): return a * b # 如果你不传参数也要加括号,默认就是默认值 @logger() def divide(a, b): return a / b print(multiply(3, 4)) print(divide(10, 2)) ```这个结构非常清晰,外层函数拿参数,返回内层装饰器,内层装饰器拿原函数,返回包装后的函数。
当然,也有一种不需要括号也支持可选参数,我们可以通过判断输入类型来处理: ```python import functools def logger(func_or_prefix): if callable(func_or_prefix): # 直接传进来的是函数,说明没加括号,默认前缀 @functools.wraps(func_or_prefix) def wrapper(*args, **kwargs): print(f"[LOG] 调用函数: {func_or_prefix.__name__}") return func_or_prefix(*args, **kwargs) return wrapper else: # 传进来的是前缀字符串,返回真正的装饰器 prefix = func_or_prefix def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"{prefix} 调用函数: {func.__name__}") return func(*args, **kwargs) return wrapper return decorator # 这样两种写法都支持 @logger def add(a, b): return a + b @logger("[DEBUG]") def subtract(a, b): return a - b print(add(1, 2)) print(subtract(5, 3)) ```这种方式比较灵活,用户可以根据自己的需求来选择,加不加括号都可以用,非常方便。
## 三、类装饰器除了用函数写装饰器,我们也可以用类来写装饰器。类装饰器的优点在于它可以更好的维护状态,比如我们要统计一个函数被调用了多少次,用类来写就很方便。
类装饰器的实现只需要:在 __init__ 里接收函数,然后实现 __call__ 方法作为包装器。
这里注意要用 functools.update_wrapper(self, func) 来更新类实例的元信息,和函数那里用 wraps 是一个道理。这样就能保留原来函数的文档和名称信息,方便调试。
__call__(func) 返回 self,然后当调用的时候再执行 __call__(*args, **kwargs),这样就能拿到参数,也能统计次数了。
## 四、多个装饰器的叠加顺序
很多时候我们会给一个函数叠加多个装饰器,那么装饰器执行顺序是什么呢?很多人容易搞混。其实记住一句话:从外到内装饰,从内到外执行。
举个例子: ```python def decorator_a(func): def wrapper(): print("进入装饰器A") func() print("退出装饰器A") return wrapper def decorator_b(func): def wrapper(): print("进入装饰器B") func() print("退出装饰器B") return wrapper @decorator_a @decorator_b def hello(): print("Hello!") hello() ``` 输出结果是: ``` 进入装饰器A 进入装饰器B Hello! 退出装饰器B 退出装饰器A ``` 对,所以顺序就是:外层装饰器先装饰,然后内层装饰器后装饰。执行的时候,外层先进入,然后内层进入,然后执行原函数,然后内层退出,外层退出。记住这个顺序就不会错了。 ## 五、装饰器装饰类现在很多场景下,我们可能需要装饰类,比如给类的所有方法都加上日志,或者统计所有方法的调用次数,用装饰器装饰类,其实非常简单,因为装饰器就是接收一个输入,返回一个包装后的对象,所以给类做装饰很简单。
举个例子,我们写一个给类所有公开方法都增加日志的装饰器: ```python import functools def log_all_methods(cls): for name, attr in cls.__dict__.items(): if callable(attr) and not name.startswith('_'): # 给每个公开方法增加日志 setattr(cls, name, logger()(attr)) return cls def logger(prefix="[LOG]"): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"{prefix} 调用方法: {func.__name__}") return func(*args, **kwargs) return wrapper return decorator @log_all_methods class Calculator: def add(self, a, b): return a + b def multiply(self, a, b): return a * b calc = Calculator() print(calc.add(1, 2)) print(calc.multiply(3, 4)) ``` 对,这样,我们遍历了类上面所有公开方法,都给加上日志,然后返回修改后的类,这样就能完成类装饰。非常方便。 ## 六、实战案例一:缓存装饰器我们写一个缓存装饰器,缓存函数的返回值,对于参数相同的调用,直接返回缓存结果,不用重新计算,这是一个非常常用的场景,特别是对于计算密集型函数非常有用。
```python import functools def cache(func): cache_dict = {} @functools.wraps(func) def wrapper(*args, **kwargs): # 把参数变成一个可哈希的键,需要处理 kwargs key = args + tuple(sorted(kwargs.items())) if key not in cache_dict: result = func(*args, **kwargs) cache_dict[key] = result print(f"缓存未命中,计算并缓存: {key}") else: print(f"缓存命中,直接返回: {key}") return cache_dict[key] wrapper.cache = cache_dict return wrapper @cache def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2) print(fib(10)) ```这个函数如果不缓存,计算斐波那契数的时候,会重复计算很多中间值,但是用了缓存之后,效率提高了几十上百倍,你可以自己试试速度。当然,Python 标准库也有内置的 lru_cache,原理跟我们这个差不多,但是我们自己实现了一个简单版本,理解原理之后,就可以根据自己需求定制。
做 Web 开发的时候,很多接口需要判断用户是否登录,是否有权限,所以用装饰器写权限校验就很方便,我们来模拟一下:
```python import functools # 模拟当前登录用户 current_user = {"name": "anonymous", "role": "user"} def require_role(role): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if current_user["role"] != role: raise PermissionError(f"需要 {role} 权限才能访问该功能") return func(*args, **kwargs) return wrapper return decorator @require_role("admin") def delete_user(user_id): return f"删除用户 {user_id} 成功" # 测试:用户权限不够 try: print(delete_user(123)) # 这里会抛出异常 except PermissionError as e: print(e) # 切换为 admin 之后才能访问 current_user["role"] = "admin" print(delete_user(123)) ```这样写出来之后,代码非常清晰,每个接口的权限要求一目了然,不用写一堆 if else 在函数里面,把权限校验这个逻辑抽出来,代码更加干净,也更容易维护。
我们经常需要统计一个函数运行了多长时间,来分析性能瓶颈,我们写一个这样的装饰器: ```python import time import functools def timeit(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"函数 {func.__name__} 运行时间: {(end - start):.6f} 秒") return result return wrapper @timeit def slow_function(): # 模拟一个慢函数 for i in range(1000000): pass return "done" slow_function() ```
运行一下你就能看到函数运行了多长时间,非常方便,而且所有函数都可以用这个装饰器,不用每个函数都写一遍 start、end,非常简洁。
## 九、总结装饰器是 Python 中非常强大的一个特性,理解了装饰器的原理,你就可以写出更加优雅、更加干净的代码,不用修改原函数,就能增加各种功能,这就是面向切面编程思想的体现。
本篇文章我们从最基础出发,一步步讲了:
1. 基础装饰器回顾,说明 functools.wraps 的重要性
2. 带参数的装饰器怎么写,同时支持可选参数的两种写法
3. 类装饰器的写法,以及带参数类装饰器的实现
4. 多个装饰器叠加时的执行顺序
5. 如何用装饰器装饰类,批量修改类方法
6. 三个实用案例:缓存、权限校验、运行计时。
其实装饰器的应用还有很多,比如 Flask、Django 这些主流 Web 框架里面也大量用到了装饰器,理解了这些核心点之后,你就可以自己写满足需求的装饰器,让代码更加简洁优雅。
希望本文能帮助你对装饰器有更深的理解,写出更加优雅的 Python 代码。
