本篇文章详细介绍了 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()
严格来说,装饰器只是语法糖。如前所述,装饰器可以像常规的可调用对象那样调用,传入另一个函数。
装饰器有以下三个基本性质:
- 装饰器是一个函数或可调用对象(例如,类创建出来的对象);
- 装饰器可以把被装饰的函数替换成其它函数;
- 装饰器在加载模块时立即执行。
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. 装饰器定义完成”,表示装饰器函数已经定义好了
- 然后立即打印 “装饰器函数正在执行…”,这说明装饰器在被装饰的函数定义时就已经执行了
- 接着打印 “2. 函数定义完成”
- 最后当我们实际调用 say_hello() 时,才执行包装函数和原始函数的内容。
3 装饰器函数工作的基础:闭包
闭包允许函数记住并访问创建它的环境中的变量,即使这些变量在函数执行时不在其作用域内。
闭包由两部分组成:
- 一个外部函数,它定义了一些变量;
- 一个内部函数,它引用了外部函数的局部变量
当外部函数返回内部函数时,内部函数会"记住"外部函数作用域中的变量,从而形成闭包。
看一个简单示例:
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
已经执行完毕。
需要注意的是:
- 如果内部函数使用外部函数的变量是可变对象,例如列表、字典等,内部函数可直接修改外部函数的变量。
- 如果内部函数使用外部函数的变量是不可变类型,例如数值、字符串、元组等,那么在内部函数必须使用
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 装饰器函数的语法
- 基本的装饰器语法
def simple_decorator(func):
def wrapper():
print("简单装饰器 - 函数调用前")
result = func()
print("简单装饰器 - 函数调用后")
return result
return wrapper
@simple_decorator
def greet():
print("你好!")
return "返回值"
- 带参数的装饰器函数
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)
。
- 装饰器链。即被装饰器的函数上使用多个装饰器。
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))
- 被装饰的函数带参数
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 标准库中的装饰器
标准库中最吸引人的几个装饰器,即 cache
、lru_cache
和 singledispatch
,均来自 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
的注意事项:
- 只适用于纯函数(相同输入总是产生相同输出);
- 参数必须是可哈希的(hashable);
- 需要根据实际情况设置合适的缓存大小。
在Python中,可哈希(hashable)的参数是指那些具有以下特性的对象:
- 不可变类型:
- 数字类型:int, float, bool
- 字符串:str
- 元组:tuple(但只有当其所有元素都是可哈希的)
- 冻结集合:frozenset
- 不可哈希的类型:
- 列表: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. 日志记录
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 万名程序员找到自己的兴趣方向。