当前位置:首页 > Python > 正文内容

Python 装饰器进阶:从入门到实战,写出更灵活的函数增强技巧

admin2小时前Python2
# 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__ 方法作为包装器。

举个例子,我们写一个统计调用次数的装饰器: ```python import functools class Counter: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"函数 {self.func.__name__} 已经被调用 {self.count} 次") return self.func(*args, **kwargs) @Counter def hello(): print("Hello World!") hello() hello() hello() ```

这里注意要用 functools.update_wrapper(self, func) 来更新类实例的元信息,和函数那里用 wraps 是一个道理。这样就能保留原来函数的文档和名称信息,方便调试。

那如果我们要给类装饰器传递参数呢?调整一下结构就可以了: ```python import functools class Counter: def __init__(self, prefix): self.prefix = prefix self.count = 0 def __call__(self, func): self.func = func functools.update_wrapper(self, func) return self def __call__(self, *args, **kwargs): self.count += 1 print(f"{self.prefix} 函数 {self.func.__name__} 已经被调用 {self.count} 次") return self.func(*args, **kwargs) @Counter("[调用次数统计] ") def hello(): print("Hello World!") ``` 对,这样,当装饰的时候 __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 代码。

相关文章

[Python 教程] OpenCV 绘图教程:图形与文本标注

OpenCV 绘图教程:图形与文本标注本文介绍如何在 OpenCV 中绘制各种图形和添加文本,用于图像标注和可视化。一、绘制基本图形1.1 创建画布import cv2 import&nb...

[Python 教程] NumPy 数组操作详解

NumPy 数组操作详解 NumPy 是 Python 科学计算的基础库,提供高性能的多维数组对象。本文详细介绍 NumPy 数组的核心操作。 一、创建数组 import numpy as np...

[Python 教程] Matplotlib 数据可视化教程

Matplotlib 数据可视化教程 Matplotlib 是 Python 最常用的绘图库。本文介绍常用图表的绘制方法。 一、基础设置 import matplotlib.pyplot as pl...

[Python 教程] Python 多线程编程指南

Python 多线程编程指南 Python 的 threading 模块提供多线程支持。本文介绍多线程编程的基础和实用技巧。 一、创建线程 import threading import time...

[Python 教程] Python 网络请求与爬虫基础

Python 网络请求与爬虫基础 requests 是 Python 最常用的 HTTP 库。本文介绍网络请求和爬虫的基础知识。 一、基础请求 import requests # GET 请求 r...

Python 装饰器实用技巧:从入门到精通

装饰器是 Python 最强大的特性之一,但也是很多开发者感到困惑的概念。简单来说,装饰器是一个函数,它接受另一个函数作为输入,并返回一个新的函数。使用装饰器,你可以在不修改原函数代码的情况下,为其添...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。