开篇:Python 的 “函数魔法”
你有没有想过,为什么 Python 中的函数可以像普通变量一样传递?为什么可以在函数内部定义另一个函数?为什么可以用 @ 符号修饰函数并增强其功能?这些问题的答案都与闭包和装饰器有关。
闭包和装饰器是 Python 中最具表现力和灵活性的特性之一,它们是高阶函数的典型应用,广泛应用于:
- 代码复用:将公共功能封装为装饰器,一次定义,多次使用
- 功能增强:在不修改原有代码的情况下增强函数的功能
- 权限控制:添加身份验证、API 密钥验证等
- 日志记录:记录函数的调用信息和运行状态
- 性能优化:添加缓存、限流等功能
- 代码简化:将复杂的功能封装为简单的装饰器
本文将从基础概念出发,系统讲解闭包和装饰器的原理、实现方法和调用方式,并结合自带装饰器和实战示例,带你全面掌握这两个核心特性。
一、闭包:函数的 “记忆”
1.1 什么是闭包?
闭包是一种函数对象,它包含了函数的定义和函数执行时的环境。换句话说,闭包可以 “记住” 函数定义时所在的作用域,即使函数在作用域外部被调用,也可以访问该作用域中的变量。
1.2 闭包的基本组成
闭包通常由以下两部分组成:
- 内部函数:在外部函数内部定义的函数
- 自由变量:内部函数使用但未在内部函数中定义的变量
1.3 闭包的实现条件
要创建一个闭包,必须满足以下条件:
- 必须有一个嵌套函数
- 内部函数必须引用外部函数的变量
- 外部函数必须返回内部函数
1.4 闭包的基本示例
# 外部函数
def outer_function(x):
# 自由变量
a = 10
# 内部函数
def inner_function(y):
# 引用外部函数的变量 x 和 a
return x + y + a
# 返回内部函数
return inner_function
# 创建闭包
closure = outer_function(5)
# 调用闭包
result = closure(3)
print(result) # 输出:18
1.5 闭包的工作原理
在上面的示例中,当 outer_function(5) 被调用时,它创建了一个作用域,其中包含变量 x=5 和 a=10。然后它返回 inner_function 对象。当 closure(3) 被调用时,inner_function 仍然可以访问 x=5 和 a=10,即使 outer_function 已经执行完毕。
1.6 闭包的特性
- 记忆性:闭包可以记住外部函数的变量和作用域
- 延迟执行:闭包只有在被调用时才会执行
- 封装性:闭包可以封装变量和功能,提高代码的模块化程度
1.7 闭包的应用场景
- 计数器:实现一个自增的计数器
- 延迟计算:只在需要时才计算结果
- 配置函数:根据不同的配置创建不同的函数
- 装饰器:闭包是装饰器的基础
1.7.1 计数器示例
def counter():
count = 0
def increment():
nonlocal count # 使用 nonlocal 关键字声明变量为非局部变量
count += 1
return count
return increment
# 创建计数器
count1 = counter()
print(count1()) # 输出:1
print(count1()) # 输出:2
print(count1()) # 输出:3
# 创建另一个计数器
count2 = counter()
print(count2()) # 输出:1
1.7.2 配置函数示例
def power(n):
def calculate(x):
return x ** n
return calculate
# 创建平方函数
square = power(2)
print(square(3)) # 输出:9
# 创建立方函数
cube = power(3)
print(cube(3)) # 输出:27
二、装饰器:函数的 “增强魔法”
2.1 什么是装饰器?
装饰器是一种用于修改或增强函数或类功能的高阶函数,它接受一个函数作为参数,并返回一个新的函数,新函数包含了原函数的功能和装饰器添加的功能。
2.2 装饰器的基本语法
# 定义装饰器
def decorator(func):
def wrapper():
# 在原函数执行前添加的功能
print("Before function execution")
# 执行原函数
func()
# 在原函数执行后添加的功能
print("After function execution")
# 返回新函数
return wrapper
# 使用装饰器
@decorator
def hello():
print("Hello, World!")
# 调用函数
hello()
2.3 装饰器的工作原理
装饰器的工作原理是函数的嵌套和闭包:
- 当定义
@decorator时,Python 会将被装饰的函数hello作为参数传递给decorator函数 decorator函数返回wrapper函数- 当调用
hello()时,实际上是调用wrapper()函数
2.4 装饰器的执行顺序
当一个函数被多个装饰器装饰时,装饰器的执行顺序是从下到上。
def decorator1(func):
def wrapper():
print("Decorator 1 Before")
func()
print("Decorator 1 After")
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2 Before")
func()
print("Decorator 2 After")
return wrapper
@decorator1
@decorator2
def hello():
print("Hello, World!")
hello()
2.5 装饰器的执行结果
Decorator 1 Before
Decorator 2 Before
Hello, World!
Decorator 2 After
Decorator 1 After
三、装饰器的高级用法
3.1 装饰器带参数
如果被装饰的函数带有参数,需要在 wrapper 函数中接收并传递这些参数。
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function execution")
result = func(*args, **kwargs)
print("After function execution")
return result
return wrapper
@decorator
def add(a, b):
return a + b
result = add(1, 2)
print(result) # 输出:3
3.2 装饰器带返回值
如果被装饰的函数有返回值,需要在 wrapper 函数中捕获并返回。
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function execution")
result = func(*args, **kwargs)
print("After function execution")
return result
return wrapper
@decorator
def multiply(a, b):
return a * b
result = multiply(3, 4)
print(result) # 输出:12
3.3 装饰器本身带参数
如果装饰器需要传递参数,需要在原装饰器外再嵌套一层函数。
def decorator_with_args(prefix):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{prefix} Before function execution")
result = func(*args, **kwargs)
print(f"{prefix} After function execution")
return result
return wrapper
return decorator
@decorator_with_args("DEBUG:")
def divide(a, b):
return a / b
result = divide(10, 2)
print(result) # 输出:5
3.4 保留原函数的元信息
使用装饰器后,原函数的元信息(如函数名、文档字符串等)会丢失,可以使用 functools.wraps 保留这些信息。
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function"""
result = func(*args, **kwargs)
return result
return wrapper
@decorator
def hello():
"""Hello function"""
print("Hello, World!")
print(f"Function name: {hello.__name__}") # 输出:hello
print(f"Function docstring: {hello.__doc__}") # 输出:Hello function
3.5 类装饰器
除了函数装饰器,还可以使用类装饰器,将装饰器封装为一个类。
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Before function execution")
result = self.func(*args, **kwargs)
print("After function execution")
return result
@Decorator
def subtract(a, b):
return a - b
result = subtract(5, 3)
print(result) # 输出:2
四、Python 自带的装饰器
Python 提供了一些内置的装饰器,用于简化开发。
4.1 @staticmethod:静态方法
静态方法是属于类的方法,不依赖于类的实例,可以直接通过类名调用。
class Math:
@staticmethod
def add(a, b):
return a + b
# 直接通过类名调用
print(Math.add(1, 2)) # 输出:3
# 通过实例调用
math = Math()
print(math.add(3, 4)) # 输出:7
4.2 @classmethod:类方法
类方法是属于类的方法,第一个参数是 cls(指向类本身),可以访问和修改类属性。
class Counter:
count = 0
def __init__(self):
Counter.count += 1
@classmethod
def get_count(cls):
return cls.count
# 创建实例
c1 = Counter()
c2 = Counter()
c3 = Counter()
# 调用类方法
print(Counter.get_count()) # 输出:3
4.3 @property:属性装饰器
属性装饰器是将方法转换为属性,让方法可以像属性一样访问,主要用于封装计算属性。
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
import math
return math.pi * self._radius ** 2
# 使用示例
circle = Circle(5)
print(circle.radius) # 输出:5
print(circle.area) # 输出:78.53981633974483
# 修改半径
circle.radius = 10
print(circle.radius) # 输出:10
print(circle.area) # 输出:314.1592653589793
4.4 @functools.lru_cache:缓存装饰器
缓存装饰器是用于缓存函数的结果,避免重复计算,提高函数的运行效率。
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# 第一次调用,需要计算
print(fibonacci(10)) # 输出:55
# 第二次调用,直接从缓存中获取
print(fibonacci(10)) # 输出:55
4.5 @functools.wraps:保留元信息装饰器
@functools.wraps 用于保留原函数的元信息,如函数名、文档字符串等。
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def hello():
"""Hello function"""
pass
print(hello.__name__) # 输出:hello
print(hello.__doc__) # 输出:Hello function
五、装饰器的实战应用
5.1 日志记录装饰器
from functools import wraps
import datetime
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 记录请求时间
request_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 记录请求信息
print(f"[LOG] {request_time} - Function: {func.__name__}, Args: {args}, Kwargs: {kwargs}")
# 执行原函数
result = func(*args, **kwargs)
# 记录响应信息
print(f"[LOG] {request_time} - Function: {func.__name__}, Result: {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
@log_decorator
def multiply(a, b):
return a * b
add(1, 2)
multiply(3, 4)
5.2 API 密钥验证装饰器
from functools import wraps
# 合法的 API 密钥
VALID_API_KEYS = {"sk-123456", "ak-789012"}
def api_key_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 假设 API 密钥是通过 kwargs 传递的
if "api_key" not in kwargs:
return {"error": "Missing API key"}, 401
api_key = kwargs.pop("api_key")
if api_key not in VALID_API_KEYS:
return {"error": "Invalid API key"}, 401
# 调用原函数
return func(*args, **kwargs)
return wrapper
@api_key_required
def get_user_info(user_id):
return {"user_id": user_id, "name": "John Doe"}, 200
# 测试
print(get_user_info(123, api_key="sk-123456")) # 输出:({'user_id': 123, 'name': 'John Doe'}, 200)
print(get_user_info(123, api_key="invalid")) # 输出:({'error': 'Invalid API key'}, 401)
5.3 请求限流装饰器
from functools import wraps
from collections import defaultdict
import time
# 限流字典
request_count = defaultdict(list)
def rate_limit(limit=10, window=60):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 获取用户 ID(假设在 kwargs 中)
user_id = kwargs.get("user_id")
if not user_id:
return {"error": "Missing user ID"}, 400
current_time = time.time()
# 清除过期的请求
request_count[user_id] = [t for t in request_count[user_id] if current_time - t < window]
# 检查请求次数
if len(request_count[user_id]) >= limit:
return {"error": "Rate limit exceeded"}, 429
# 记录请求
request_count[user_id].append(current_time)
# 调用原函数
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(limit=3, window=10)
def send_message(user_id, message):
return {"status": "success", "message": "Message sent"}, 200
# 测试
for i in range(5):
print(send_message(user_id="123", message="Hello"))
time.sleep(2)
5.4 缓存装饰器
from functools import wraps
def cache_decorator(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
cache_key = str(args) + str(sorted(kwargs.items()))
# 检查缓存
if cache_key in cache:
print(f"[CACHE] Using cached result for {args}, {kwargs}")
return cache[cache_key]
# 执行原函数
result = func(*args, **kwargs)
# 保存到缓存
cache[cache_key] = result
print(f"[CACHE] Saving result for {args}, {kwargs}")
return result
return wrapper
@cache_decorator
def expensive_calculation(a, b):
import time
time.sleep(2) # 模拟耗时计算
return a + b
# 第一次调用
print(expensive_calculation(1, 2)) # 输出:3
# 第二次调用
print(expensive_calculation(1, 2)) # 输出:3
六、装饰器的性能优化与最佳实践
6.1 装饰器的顺序
装饰器的顺序非常重要,应该 ** 按照 “安全→限流→日志→业务→统计→异常”** 的顺序装饰:
- 安全装饰器:先验证 API 密钥和身份
- 限流装饰器:在验证后限制请求频率
- 日志装饰器:记录完整的请求信息
- 业务函数:执行核心业务逻辑
- 统计装饰器:统计业务执行的结果
- 异常处理装饰器:最后处理所有异常
6.2 避免过度装饰
虽然装饰器很强大,但不要过度装饰,过多的装饰器会增加代码的复杂度和运行时间。
6.3 使用缓存装饰器
对于频繁调用且结果不变的函数,可以使用 functools.lru_cache 装饰器缓存结果。
6.4 装饰器的参数化
将装饰器的配置参数化,提高装饰器的灵活性。
def rate_limit(limit=10, window=60):
def decorator(func):
def wrapper(*args, **kwargs):
# 限流逻辑
pass
return wrapper
return decorator
# 使用参数化装饰器
@rate_limit(limit=5, window=30)
def call_api():
pass
6.5 使用类装饰器
对于复杂的装饰器,可以使用类装饰器,提高代码的可读性和可维护性。
6.6 测试装饰器
在使用装饰器之前,应该充分测试装饰器的功能,确保它不会影响原函数的正常运行。
七、装饰器的常见问题与解决方案
7.1 忘记传递参数
问题:装饰器没有传递被装饰函数的参数。解决方案:在 wrapper 函数中使用 *args 和 **kwargs 接收和传递参数。
7.2 丢失原函数的元信息
问题:装饰器修改了原函数的名称、文档字符串等元信息。解决方案:使用 functools.wraps 保留原函数的元信息。
7.3 装饰器的执行顺序错误
问题:装饰器的执行顺序不符合预期。解决方案:注意装饰器的顺序,从下到上执行。
7.4 装饰器的参数错误
问题:装饰器本身的参数传递错误。解决方案:使用三层嵌套的装饰器处理装饰器本身的参数。
7.5 装饰器的性能问题
问题:装饰器导致函数运行速度变慢。解决方案:
- 简化装饰器的逻辑
- 避免在装饰器中执行耗时的操作
- 对于频繁调用的函数,使用缓存装饰器
八、总结:闭包与装饰器的关系
闭包和装饰器是密切相关的:
- 闭包是装饰器的基础:装饰器本质上是一个返回函数的闭包
- 装饰器是闭包的应用:装饰器利用闭包的特性来增强函数的功能
- 两者都是高阶函数的应用:它们都接受函数作为参数,并返回一个新的函数
闭包和装饰器是 Python 中最具表现力和灵活性的特性之一,掌握它们能帮助你写出更加简洁、高效、可维护的代码。通过本文的学习,你已经掌握了闭包和装饰器的原理、实现方法和调用方式,并结合自带装饰器和实战示例进行了深入的探索。
在实际开发中,要遵循装饰器的最佳实践,合理安排装饰器的顺序,保留原函数的元信息,避免过度装饰,使用参数化装饰器提高灵活性。同时,要注意装饰器的性能问题,对于频繁调用的函数,使用缓存装饰器来提高运行效率。
872

被折叠的 条评论
为什么被折叠?



