python避坑-装饰器的执行时机陷阱

内容分享4天前发布
0 0 0

python避坑-装饰器的执行时机陷阱

您在使用python的过程中遇到过哪些“坑”呢?欢迎在留言区分享讨论,也欢迎关注收藏,多谢.

装饰器是Python中一个强劲而优雅的特性,它让我们能够在不修改原函数的情况下增强函数的功能。但装饰器的执行时机常常让人困惑,特别是当装饰器有参数或者需要访问函数属性时。我曾经由于不理解装饰器的执行时机而写出了一个只在某些情况下工作的bug。

装饰器的基本执行时机

第一,让我们理解装饰器的基本执行时机:

def my_decorator(func):
    print("装饰器函数被调用")
    def wrapper(*args, **kwargs):
        print("包装函数被调用")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

# 输出:
# 装饰器函数被调用

# 调用函数
say_hello()
# 输出:
# 包装函数被调用
# Hello!

注意:装饰器函数在函数定义时就被调用了,而不是在函数被调用时。

带参数的装饰器

带参数的装饰器有更复杂的执行时机:

def decorator_with_args(arg1, arg2):
    print(f"装饰器工厂被调用,参数: {arg1}, {arg2}")
    
    def actual_decorator(func):
        print(f"实际装饰器被调用,函数: {func.__name__}")
        
        def wrapper(*args, **kwargs):
            print(f"包装函数被调用,参数: {arg1}, {arg2}")
            return func(*args, **kwargs)
        
        return wrapper
    
    return actual_decorator

@decorator_with_args("hello", "world")
def say_hello():
    print("Hello!")

# 输出:
# 装饰器工厂被调用,参数: hello, world
# 实际装饰器被调用,函数: say_hello

# 调用函数
say_hello()
# 输出:
# 包装函数被调用,参数: hello, world
# Hello!

实际开发中的陷阱

1. 闭包变量捕获问题

# 错误的装饰器实现
def create_decorators():
    decorators = []
    for i in range(3):
        def decorator(func):
            def wrapper(*args, **kwargs):
                print(f"装饰器 {i} 被调用")  # 问题:i的值是循环结束后的值
                return func(*args, **kwargs)
            return wrapper
        decorators.append(decorator)
    return decorators

# 使用装饰器
decorators = create_decorators()

@decorators[0]
def func1():
    print("func1")

@decorators[1]
def func2():
    print("func2")

# 调用函数
func1()  # 输出: 装饰器 2 被调用
func2()  # 输出: 装饰器 2 被调用

2. 函数属性丢失问题

# 装饰器没有保留函数属性
def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def my_function():
    """这是一个测试函数"""
    pass

# 函数属性丢失了
print(my_function.__name__)  # wrapper
print(my_function.__doc__)   # None

3. 装饰器顺序问题

# 装饰器的顺序很重大
def decorator_a(func):
    print("装饰器A被调用")
    def wrapper(*args, **kwargs):
        print("A: 函数调用前")
        result = func(*args, **kwargs)
        print("A: 函数调用后")
        return result
    return wrapper

def decorator_b(func):
    print("装饰器B被调用")
    def wrapper(*args, **kwargs):
        print("B: 函数调用前")
        result = func(*args, **kwargs)
        print("B: 函数调用后")
        return result
    return wrapper

@decorator_a
@decorator_b
def my_function():
    print("函数执行")

# 输出:
# 装饰器B被调用
# 装饰器A被调用

# 调用函数
my_function()
# 输出:
# A: 函数调用前
# B: 函数调用前
# 函数执行
# B: 函数调用后
# A: 函数调用后

正确的解决方案

1. 使用functools.wraps

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def my_function():
    """这是一个测试函数"""
    pass

# 函数属性被保留了
print(my_function.__name__)  # my_function
print(my_function.__doc__)   # 这是一个测试函数

2. 正确处理闭包变量

# 正确的装饰器实现
def create_decorators():
    decorators = []
    for i in range(3):
        def decorator_factory(index):
            def decorator(func):
                @wraps(func)
                def wrapper(*args, **kwargs):
                    print(f"装饰器 {index} 被调用")
                    return func(*args, **kwargs)
                return wrapper
            return decorator
        decorators.append(decorator_factory(i))
    return decorators

# 使用装饰器
decorators = create_decorators()

@decorators[0]
def func1():
    print("func1")

@decorators[1]
def func2():
    print("func2")

# 调用函数
func1()  # 输出: 装饰器 0 被调用
func2()  # 输出: 装饰器 1 被调用

3. 使用类装饰器

class MyDecorator:
    def __init__(self, func):
        self.func = func
        # 保留函数属性
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__
    
    def __call__(self, *args, **kwargs):
        print("装饰器被调用")
        return self.func(*args, **kwargs)

@MyDecorator
def my_function():
    """这是一个测试函数"""
    pass

# 函数属性被保留了
print(my_function.__name__)  # my_function
print(my_function.__doc__)   # 这是一个测试函数

实际应用示例

1. 日志装饰器

import functools
import time
import logging

def log_execution_time(func):
    """记录函数执行时间的装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"{func.__name__} 执行时间: {execution_time:.4f}秒")
        return result
    return wrapper

@log_execution_time
def slow_function():
    time.sleep(1)
    return "完成"

# 测试
result = slow_function()
print(result)

2. 缓存装饰器

import functools

def cache(func):
    """简单的缓存装饰器"""
    cache_dict = {}
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 创建缓存键
        cache_key = str(args) + str(sorted(kwargs.items()))
        
        if cache_key in cache_dict:
            print(f"缓存命中: {func.__name__}")
            return cache_dict[cache_key]
        
        print(f"计算: {func.__name__}")
        result = func(*args, **kwargs)
        cache_dict[cache_key] = result
        return result
    
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 测试
print(fibonacci(10))
print(fibonacci(10))  # 第二次调用会使用缓存

3. 权限检查装饰器

import functools

def require_permission(permission):
    """权限检查装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 模拟权限检查
            user_permissions = getattr(wrapper, 'user_permissions', [])
            if permission not in user_permissions:
                raise PermissionError(f"需要权限: {permission}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_permission("admin")
def delete_user(user_id):
    return f"删除用户 {user_id}"

# 测试
try:
    result = delete_user(123)
    print(result)
except PermissionError as e:
    print(f"权限错误: {e}")

4. 重试装饰器

import functools
import time
import random

def retry(max_attempts=3, delay=1):
    """重试装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise e
                    print(f"尝试 {attempt + 1} 失败: {e}")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
    if random.random() < 0.7:  # 70%的概率失败
        raise Exception("随机失败")
    return "成功"

# 测试
try:
    result = unreliable_function()
    print(result)
except Exception as e:
    print(f"最终失败: {e}")

调试技巧

def debug_decorator(func):
    """调试装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")
        print(f"参数: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"返回值: {result}")
        return result
    return wrapper

@debug_decorator
def test_function(a, b, c=None):
    return a + b + (c or 0)

# 测试
test_function(1, 2, c=3)

最佳实践

  • 使用functools.wraps:保留函数属性
  • 理解执行时机:装饰器在函数定义时执行
  • 注意装饰器顺序:从内到外执行
  • 正确处理闭包变量:避免变量捕获问题
  • 测试装饰器:确保装饰器按预期工作

写在最后

装饰器的执行时机是Python中一个微妙但重大的概念。理解这个时机对于写出正确的装饰器代码很重大。

记住这些要点:

  • 装饰器在函数定义时执行:不是调用时
  • 使用functools.wraps:保留函数属性
  • 注意装饰器顺序:从内到外执行
  • 正确处理闭包变量:避免变量捕获问题

掌握这些概念不仅能帮你避免bug,还能让你更好地利用装饰器的强劲功能。

您在使用python的过程中这个问题或者类似的其他“坑”吗,欢迎在留言区分享讨论,也欢迎关注收藏,我们将持续分享更多好文,多谢.

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...