Python面试官:你来解释一下函数装饰器

在这里插入图片描述
本篇文章详细介绍了 Python 当中的解释器,包括基本概念、闭包、装饰器的各种语法、标准库中几个常用的装饰器,以及装饰器函数的使用场景。

聚焦全栈开发,人工智能领域。每周发布 2-4 篇技术博客,希望分享的内容对你有帮助。

1 什么是装饰器?

装饰器是一种可调用对象,其参数是另外一个参数(被修饰的函数)。

装饰器可能会对被装饰的函数做些处理,然后返回函数,或者把函数替换成另一个函数或可调用对象。

例如,下面是一个最简单的装饰器函数:

def demo(func):
    def inner():
        print("running inner()")
    return inner

@demo
def hello():
    print("running hello()")

# 上面的写法等价于
hello = demo(hello)
hello()	# Output: running inner()

严格来说,装饰器只是语法糖。如前所述,装饰器可以像常规的可调用对象那样调用,传入另一个函数。

装饰器有以下三个基本性质:

  1. 装饰器是一个函数或可调用对象(例如,类创建出来的对象);
  2. 装饰器可以把被装饰的函数替换成其它函数;
  3. 装饰器在加载模块时立即执行。

2 何时执行函数装饰器?

装饰器的一个关键性质是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(例如,当Python加载模块时)。下面看一个例子:

def my_decorator(func):
    print("装饰器函数正在执行...")
    def wrapper():
        print("包装函数被调用")
        return func()
    return wrapper

print("1. 装饰器定义完成")

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

print("2. 函数定义完成")

print("3. 开始调用函数")
say_hello() 

这段代码的输出如下:

1. 装饰器定义完成
装饰器函数正在执行...
2. 函数定义完成
3. 开始调用函数
包装函数被调用
Hello!

从输出结果我们可以看到装饰器的执行时机:

  1. 首先打印 “1. 装饰器定义完成”,表示装饰器函数已经定义好了
  2. 然后立即打印 “装饰器函数正在执行…”,这说明装饰器在被装饰的函数定义时就已经执行了
  3. 接着打印 “2. 函数定义完成”
  4. 最后当我们实际调用 say_hello() 时,才执行包装函数和原始函数的内容。

3 装饰器函数工作的基础:闭包

闭包允许函数记住并访问创建它的环境中的变量,即使这些变量在函数执行时不在其作用域内。

闭包由两部分组成:

  1. 一个外部函数,它定义了一些变量;
  2. 一个内部函数,它引用了外部函数的局部变量

当外部函数返回内部函数时,内部函数会"记住"外部函数作用域中的变量,从而形成闭包。

看一个简单示例:

def outer_function(x):
    # 外部函数中定义的局部变量
    def inner_function(y):
        # 内部函数引用了外部函数的变量x
        return x + y
    # 返回内部函数
    return inner_function

# 创建闭包
closure = outer_function(10)
# 调用闭包
result = closure(5)  # 结果为15

在这个例子中,inner_function记住了变量x的值,即使outer_function已经执行完毕。

需要注意的是:

  1. 如果内部函数使用外部函数的变量是可变对象,例如列表、字典等,内部函数可直接修改外部函数的变量。
  2. 如果内部函数使用外部函数的变量是不可变类型,例如数值、字符串、元组等,那么在内部函数必须使用 nonlocal关键字声明外部函数中的不可变对象。

一个需要使用 nonlocal关键字的例子:

def counter():
    count = 0
    def increment():
        nonlocal count  # 声明count为非局部变量
        count += 1
        return count
    return increment

my_counter = counter()
print(my_counter())  # 输出: 1
print(my_counter())  # 输出: 2

4 装饰器函数的语法

  1. 基本的装饰器语法
def simple_decorator(func):
    def wrapper():
        print("简单装饰器 - 函数调用前")
        result = func()
        print("简单装饰器 - 函数调用后")
        return result
    return wrapper

@simple_decorator
def greet():
    print("你好!")
    return "返回值"
  1. 带参数的装饰器函数
def decorator_with_args(*args, **kwargs):
    print(f"装饰器参数: {args}, {kwargs}")
    def decorator(func):
        def wrapper():
            print(f"带参数的装饰器 - 函数调用前")
            result = func()
            print(f"带参数的装饰器 - 函数调用后")
            return result
        return wrapper
    return decorator

@decorator_with_args("参数1", "参数2", key="value")
def greet_with_args():
    print("带参数的装饰器示例")
    return "返回值"

对于带参数的装饰器函数,需要在装饰器内部多写一层函数用来接受被装饰的函数,在我们的例子当中就是 decorator(func)

  1. 装饰器链。即被装饰器的函数上使用多个装饰器。
def decorator1(func):
    def wrapper():
        print("装饰器1 - 前")
        result = func()
        print("装饰器1 - 后")
        return result
    return wrapper

def decorator2(func):
    def wrapper():
        print("装饰器2 - 前")
        result = func()
        print("装饰器2 - 后")
        return result
    return wrapper

@decorator1
@decorator2
def greet_with_chain():
    print("装饰器链示例")
    return "返回值"

当在被装饰函数上使用多个装饰器时,上面的例子就相当于:

greet_with_chain = decorator1(decorator2(greet_with_chain))

  1. 被装饰的函数带参数
def param_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"带参数的被装饰函数 - 函数调用前")
        print(f"接收到的参数: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"带参数的被装饰函数 - 函数调用后")
        return result
    return wrapper

@param_decorator
def greet_with_params(name, greeting="你好", times=1):
    for _ in range(times):
        print(f"{greeting}, {name}!")
    return f"已向{name}问候{times}次"


result = greet_with_params("小明", greeting="早上好", times=2)
print(f"返回值: {result}")

*args**kwargs 确保装饰器可以处理任意数量和类型的参数。

输出结果:

带参数的被装饰函数 - 函数调用前
接收到的参数: args=('小明',), kwargs={'greeting': '早上好', 'times': 2}
早上好, 小明!
早上好, 小明!
带参数的被装饰函数 - 函数调用后
返回值: 已向小明问候2

5 标准库中的装饰器

标准库中最吸引人的几个装饰器,即 cachelru_cachesingledispatch,均来自 functools 模块。

5.1 使用 functools.cache 做备忘

functools.cache装饰器实现了备忘(memoization)。这是一项优化技术,能把耗时的函数得到的结果保存起来,避免传入相同的参数时重复计算。

举一个例子。

我们知道,使用递归计算斐波那契数列是非常耗时的:

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f"{k}={v!r}" for k, v in kwargs.items())
        arg_str = ", ".join(arg_lst)
        print(f"[{elapsed:0.8f}s] {name}({arg_str}) -> {result!r}")
        return result
    return clocked

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

if __name__ == "__main__":
    print(fibonacci(6))

输出结果:

[0.00000020s] fibonacci(0) -> 0
[0.00000300s] fibonacci(1) -> 1
[0.00019720s] fibonacci(2) -> 1
[0.00000040s] fibonacci(1) -> 1
[0.00000030s] fibonacci(0) -> 0
[0.00000090s] fibonacci(1) -> 1
[0.00017510s] fibonacci(2) -> 1
[0.00022770s] fibonacci(3) -> 2
[0.00057050s] fibonacci(4) -> 3
[0.00000020s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000080s] fibonacci(1) -> 1
[0.00026570s] fibonacci(2) -> 1
[0.00046680s] fibonacci(3) -> 2
[0.00000020s] fibonacci(0) -> 0
[0.00000050s] fibonacci(1) -> 1
[0.00016590s] fibonacci(2) -> 1
[0.00000020s] fibonacci(1) -> 1
[0.00000040s] fibonacci(0) -> 0
[0.00000080s] fibonacci(1) -> 1
[0.00020440s] fibonacci(2) -> 1
[0.00036270s] fibonacci(3) -> 2
[0.00068150s] fibonacci(4) -> 3
[0.00134260s] fibonacci(5) -> 5
[0.00219590s] fibonacci(6) -> 8
8

浪费时间的地方很明显,fibonacci(1)调用了8次,fibonacci(2)调用了5次。

但是,如果我们在 fibonacci()函数上使用 functools.cache装饰器:

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

再来运行一遍的结果:

[0.00000020s] fibonacci(0) -> 0
[0.00000280s] fibonacci(1) -> 1
[0.00047300s] fibonacci(2) -> 1
[0.00000310s] fibonacci(3) -> 2
[0.00070200s] fibonacci(4) -> 3
[0.00000120s] fibonacci(5) -> 5
[0.00092510s] fibonacci(6) -> 8
8

可以看到,重复的计算确实被保存起来了。

有同学想到了,缓存是不是会占用额外的存储空间?

是的。

如果缓存较大,则functools.cache有可能耗尽所有可用内存。@cache更适合短期运行的命令行脚本使用。对于长期运行的进程,推荐使用functools.lru_cache,并合理设置maxsize参数

5.2 使用 functools.lru_cache

functools.cache 装饰器只是对较旧的 functools.lru_cache 函数的简单包装。其实,functools.lru_cache 更灵活,而且兼容 Python3.8 及之前的版本。

@lru_cache 的主要优势是可以通过 maxsize 参数限制内存用量上限。maxsize 参数的默认值相当保守,只有128,即缓存最多只能有128条。

看一个例子:

import time
from functools import lru_cache

# 1. 不使用缓存的斐波那契数列计算
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

# 2. 使用lru_cache的斐波那契数列计算
@lru_cache(maxsize=128)
def fib_cached(n):
    if n < 2:
        return n
    return fib_cached(n-1) + fib_cached(n-2)

# 3. 模拟一个耗时的计算函数
def expensive_computation(n):
    time.sleep(1)  # 模拟耗时操作
    return n * n

# 4. 使用lru_cache的耗时计算函数
@lru_cache(maxsize=32)
def expensive_computation_cached(n):
    time.sleep(1)  # 模拟耗时操作
    return n * n

# 5. 演示缓存命中率
@lru_cache(maxsize=3)
def get_user_data(user_id):
    print(f"从数据库获取用户 {user_id} 的数据")
    return f"用户 {user_id} 的数据"

print("=== 斐波那契数列计算性能对比 ===")
start_time = time.time()
result = fib(35)
end_time = time.time()
print(f"不使用缓存: fib(35) = {result}, 耗时: {end_time - start_time:.2f}秒")

start_time = time.time()
result = fib_cached(35)
end_time = time.time()
print(f"使用缓存: fib_cached(35) = {result}, 耗时: {end_time - start_time:.2f}秒")

print("\n=== 耗时计算性能对比 ===")
# 第一次调用
start_time = time.time()
result = expensive_computation(5)
end_time = time.time()
print(f"不使用缓存第一次调用: expensive_computation(5) = {result}, 耗时: {end_time - start_time:.2f}秒")

# 第二次调用相同参数
start_time = time.time()
result = expensive_computation(5)
end_time = time.time()
print(f"不使用缓存第二次调用: expensive_computation(5) = {result}, 耗时: {end_time - start_time:.2f}秒")

# 使用缓存的第一次调用
start_time = time.time()
result = expensive_computation_cached(5)
end_time = time.time()
print(f"使用缓存第一次调用: expensive_computation_cached(5) = {result}, 耗时: {end_time - start_time:.2f}秒")

# 使用缓存的第二次调用
start_time = time.time()
result = expensive_computation_cached(5)
end_time = time.time()
print(f"使用缓存第二次调用: expensive_computation_cached(5) = {result}, 耗时: {end_time - start_time:.2f}秒")

print("\n=== 缓存命中率演示 ===")
# 第一次调用
print("第一次调用用户1:", get_user_data(1))
# 第二次调用相同用户
print("第二次调用用户1:", get_user_data(1))
# 调用新用户
print("调用用户2:", get_user_data(2))
print("调用用户3:", get_user_data(3))
print("调用用户4:", get_user_data(4))  # 这会淘汰用户1的缓存
print("再次调用用户1:", get_user_data(1))  # 需要重新获取数据

# 查看缓存信息
print("\n=== 缓存统计信息 ===")
print(f"fib_cached 缓存信息: {fib_cached.cache_info()}")
print(f"expensive_computation_cached 缓存信息: {expensive_computation_cached.cache_info()}")
print(f"get_user_data 缓存信息: {get_user_data.cache_info()}") 

输出结果如下:

=== 斐波那契数列计算性能对比 ===
不使用缓存: fib(35) = 9227465, 耗时: 1.68秒
使用缓存: fib_cached(35) = 9227465, 耗时: 0.00=== 耗时计算性能对比 ===
不使用缓存第一次调用: expensive_computation(5) = 25, 耗时: 1.00秒
不使用缓存第二次调用: expensive_computation(5) = 25, 耗时: 1.00秒
使用缓存第一次调用: expensive_computation_cached(5) = 25, 耗时: 1.00秒
使用缓存第二次调用: expensive_computation_cached(5) = 25, 耗时: 0.00=== 缓存命中率演示 ===
从数据库获取用户 1 的数据
第一次调用用户1: 用户 1 的数据
第二次调用用户1: 用户 1 的数据
从数据库获取用户 2 的数据
调用用户2: 用户 2 的数据
从数据库获取用户 3 的数据
调用用户3: 用户 3 的数据
从数据库获取用户 4 的数据
调用用户4: 用户 4 的数据
从数据库获取用户 1 的数据
再次调用用户1: 用户 1 的数据

=== 缓存统计信息 ===
fib_cached 缓存信息: CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)
expensive_computation_cached 缓存信息: CacheInfo(hits=1, misses=1, maxsize=32, currsize=1)
get_user_data 缓存信息: CacheInfo(hits=1, misses=5, maxsize=3, currsize=3)

使用functools.lru_cache的注意事项:

  1. 只适用于纯函数(相同输入总是产生相同输出);
  2. 参数必须是可哈希的(hashable);
  3. 需要根据实际情况设置合适的缓存大小。

在Python中,可哈希(hashable)的参数是指那些具有以下特性的对象:

  1. 不可变类型:
  • 数字类型:int, float, bool
  • 字符串:str
  • 元组:tuple(但只有当其所有元素都是可哈希的)
  • 冻结集合:frozenset
  1. 不可哈希的类型:
  • 列表:list
  • 字典:dict
  • 集合:set
  • 字节数组:bytearray
  • 自定义类的实例(除非实现了 hash 方法)

5.3 functools.singledispatch单分派泛化函数

现在,我们想实现一个这样的函数:函数根据不同的参数类型出发不同的处理逻辑,我们有两种实现方案:

from decimal import Decimal
from fractions import Fraction

# 1. 使用传统的if-elif方式处理不同类型
def process_data(data):
    """处理不同类型的数据"""
    if isinstance(data, int):
        print(f"处理整数: {data}")
        return data * 2
    elif isinstance(data, float):
        print(f"处理浮点数: {data}")
        return data * 1.5
    elif isinstance(data, str):
        print(f"处理字符串: {data}")
        return data.upper()
    elif isinstance(data, list):
        print(f"处理列表: {data}")
        return sum(data)
    elif isinstance(data, dict):
        print(f"处理字典: {data}")
        return {k.upper(): v for k, v in data.items()}
    elif isinstance(data, Decimal):
        print(f"处理Decimal: {data}")
        return data * Decimal('2.5')
    elif isinstance(data, Fraction):
        print(f"处理Fraction: {data}")
        return data * Fraction(3, 2)
    else:
        print(f"处理未知类型: {type(data)}")
        return data

# 2. 使用字典映射方式处理不同类型
def process_data_dict(data):
    """使用字典映射处理不同类型的数据"""
    handlers = {
        int: lambda x: (print(f"处理整数: {x}"), x * 2)[1],
        float: lambda x: (print(f"处理浮点数: {x}"), x * 1.5)[1],
        str: lambda x: (print(f"处理字符串: {x}"), x.upper())[1],
        list: lambda x: (print(f"处理列表: {x}"), sum(x))[1],
        dict: lambda x: (print(f"处理字典: {x}"), {k.upper(): v for k, v in x.items()})[1],
        Decimal: lambda x: (print(f"处理Decimal: {x}"), x * Decimal('2.5'))[1],
        Fraction: lambda x: (print(f"处理Fraction: {x}"), x * Fraction(3, 2))[1]
    }
    
    handler = handlers.get(type(data))
    if handler:
        return handler(data)
    else:
        print(f"处理未知类型: {type(data)}")
        return data

你会发现我们的函数中,要么充斥着大量的 if-else语句,要么使用了很多 lambda表达式。这其实违反了“对扩展开放,对修改关闭”这条原则。学过 Java 或 C++的一定知道使用方法重载来处理,但是 Python 并不支持方法重载。这时,我们就可以使用装饰器 functools.singledispatch

代码实现:

from functools import singledispatch
from decimal import Decimal
from fractions import Fraction

# 1. 基本用法:处理不同类型的数据
@singledispatch
def process_data(data):
    """默认处理函数"""
    print(f"处理未知类型: {type(data)}")
    return data

# 2. 注册处理整数的函数
@process_data.register(int)
def _(data):
    print(f"处理整数: {data}")
    return data * 2

# 3. 注册处理浮点数的函数
@process_data.register(float)
def _(data):
    print(f"处理浮点数: {data}")
    return data * 1.5

# 4. 注册处理字符串的函数
@process_data.register(str)
def _(data):
    print(f"处理字符串: {data}")
    return data.upper()

# 5. 注册处理列表的函数
@process_data.register(list)
def _(data):
    print(f"处理列表: {data}")
    return sum(data)

# 6. 注册处理字典的函数
@process_data.register(dict)
def _(data):
    print(f"处理字典: {data}")
    return {k.upper(): v for k, v in data.items()}

# 7. 注册处理Decimal的函数
@process_data.register(Decimal)
def _(data):
    print(f"处理Decimal: {data}")
    return data * Decimal('2.5')

# 8. 注册处理Fraction的函数
@process_data.register(Fraction)
def _(data):
    print(f"处理Fraction: {data}")
    return data * Fraction(3, 2)

singledispatch机制的一个优点是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新模块中定义了新类型,则可以轻易添加一个新的自定义函数来处理新类型。

6 装饰器函数的使用场景

装饰器函数有几个常见的使用场景:

  1. 日志记录
  2. 性能计时
  3. 权限验证
  4. 缓存结果,提升程序性能
  5. 实现重试机制
  6. 参数验证
# 1. 日志记录
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"开始执行函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 执行完成")
        return result
    return wrapper

# 2. 性能计时
def timing_decorator(func):
    import time
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"函数 {func.__name__} 执行时间: {end_time - start_time:.4f}秒")
        return result
    return wrapper

# 3. 权限验证
def auth_required(func):
    def wrapper(*args, **kwargs):
        # 这里可以添加实际的认证逻辑
        if not is_authenticated():
            raise PermissionError("需要登录才能访问")
        return func(*args, **kwargs)
    return wrapper

# 4. 缓存结果
def cache_decorator(func):
    cache = {}
    def wrapper(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return wrapper

# 5. 重试机制
def retry_decorator(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        raise
                    print(f"尝试 {attempts} 失败,正在重试...")
            return None
        return wrapper
    return decorator

# 6. 参数验证
def validate_params(*validators):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for validator in validators:
                validator(*args, **kwargs)
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 使用示例
@log_decorator
@timing_decorator
def calculate_factorial(n):
    if n == 0:
        return 1
    return n * calculate_factorial(n-1)

@cache_decorator
def expensive_operation(x):
    # 模拟耗时操作
    import time
    time.sleep(1)
    return x * x

@retry_decorator(max_attempts=3)
def unreliable_operation():
    import random
    if random.random() < 0.5:
        raise Exception("随机失败")
    return "操作成功"

# 辅助函数
def is_authenticated():
    # 模拟认证状态
    return True

7 小结

本篇文章我们介绍了 Python 当中的装饰器,包括装饰器的作用、装饰器是在导入时执行的、闭包、装饰器函数的四种常见定义方法、标准库中的装饰器,以及装饰器的常见使用场景。

事实上,关于装饰器的知识这里还并不完备,例如,可以使用类来定义装饰器以及一些其它诸如 @property@classmethod等标准库的装饰器还没有介绍。将来会另外写一篇文章介绍。

好了,今天的文章就到这里了。如有帮助,记得一键三连。

聚焦全栈开发和人工智能领域,持续分享利用 AI 助力学习和工作方面的知识,愿景是帮助 1 万名程序员找到自己的兴趣方向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值