引言
在探索编写高效 Python 代码的旅程中,我们的下一站是 functools
模块。正如作者们所描述的那样:
functools
模块包含了高级函数,这些函数作用于其他函数或是返回其他函数。通常情况下,任何可调用的对象都可以作为该模块的目的来处理。
这个模块里包含了能够显著提升 Python 项目性能、可读性和灵活性的强大工具。在这篇博客中,我们将探讨这个模块中的四个方面,它们可能会改变你对下一个项目的开发方式。具体来说,我们来学习一下 lru_cache
、partial
、reduce
和 cmp_to_key
。我们会探讨这些函数的作用,并通过代码示例来加深理解。接着,我们将讨论最佳实践,帮助你充分利用这些工具。
1. 使用缓存优化性能:lru_cache
lru_cache
装饰器是一个强大的工具,用于记忆化计算,它会存储昂贵函数调用的结果,并在遇到相同的输入时重用这些结果。这可以极大地减少那些反复以相同参数调用的函数的时间复杂度。
示例:斐波那契数列
让我们使用 lru_cache
来优化斐波那契数列的计算。
from functools import lru_cache
@lru_cache(maxsize=1000)
def fibonacci(n):
"""返回第 n 个斐波那契数。"""
if n < 0:
raise ValueError("不支持负数参数。")
elif n in {0, 1}:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 使用示例
for i in range(20):
print(f"Fibonacci({i}) = {fibonacci(i)}")
# 输出:
# ================
# Fibonacci(0) = 0
# Fibonacci(1) = 1
# Fibonacci(2) = 1
# Fibonacci(3) = 2
# Fibonacci(4) = 3
# .
# .
# .
# Fibonacci(18) = 2584
# Fibonacci(19) = 4181
上面的 @lru_cache(maxsize=1000)
装饰器最多可以缓存 1000 个结果,如果缓存大小超过这个值,最久未使用的条目将被清除。
从函数中我们可以看到它是递归地计算斐波那契数列的。如果没有缓存,这将涉及冗余计算,但 lru_cache
存储了结果来避免这种情况。
性能对比
让我们通过比较有无 lru_cache
的执行时间来看看性能上的提升。
import time
from functools import lru_cache
# 带缓存
@lru_cache(maxsize=1000)
def fibonacci(n):
"""返回第 n 个斐波那契数。"""
if n < 0:
raise ValueError("不支持负数参数。")
elif n in {0, 1}:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 不带缓存
def fibonacci_no_cache(n):
if n < 0:
raise ValueError("不支持负数参数。")
elif n in {0, 1}:
return n
return fibonacci_no_cache(n - 1) + fibonacci_no_cache(n - 2)
# 计时
start_time = time.time()
# 不带缓存
print(fibonacci_no_cache(30))
print(f"不带缓存: {time.time() - start_time:.6f} 秒")
# 带缓存
start_time = time.time()
print(fibonacci(30))
print(f"带缓存: {time.time() - start_time:.6f} 秒")
上面代码的输出如下:
832040 不带缓存: 0.099412 秒 832040 带缓存: 0.000007 秒
如预期的那样,结果相同。另外,注意到性能提高了近四个数量级!这对于仅仅在函数定义上方添加一个装饰器来说,这是相当大的提升。
最棒的是,数据量越大,性能提升越明显。让我们用上面定义的函数来演示这一点。
import time
import matplotlib.pyplot as plt
from tqdm import tqdm
# 计时
times_with_cache = []
times_without_cache = []
values = range(1, 101, 5)
for n in tqdm(values):
# 不带缓存
start_time = time.time()
fibonacci_no_cache(n)
times_without_cache.append(time.time() - start_time)
for n in tqdm(values):
# 带缓存
start_time = time.time()
fibonacci(n)
times_with_cache.append(time.time() - start_time)
# 绘制结果
plt.figure(figsize=(10, 6))
plt.plot(values, times_without_cache, label='不带缓存', marker='o')
plt.plot(values, times_with_cache, label='带缓存', marker='o')
plt.title('斐波那契数计算时间(带与不带缓存)')
plt.xlabel('斐波那契数')
plt.ylabel('时间(秒)')
plt.legend()
plt.grid(True)
plt.annotate(f'不带缓存 Fibonacci(50): {times_without_cache[-1]:.6f} 秒',
xy=(values[-1], times_without_cache[-1]),
xytext=(values[-1] - 30, times_without_cache[-1] - 10),
arrowprops=dict(facecolor='black', shrink=0.05))
plt.annotate(f'带缓存 Fibonacci(50): {times_with_cache[-1]:.6f} 秒',
xy=(values[-1], times_with_cache[-1]),
xytext=(values[-1] - 30, times_with_cache[-1] + 10),
arrowprops=dict(facecolor='black', shrink=0.05))
# 保存绘图为 PNG
plt.savefig('fibonacci_timing_comparison.png')
plt.show()
2. 简化函数调用:partial
partial
函数允许你固定某些函数参数并生成新的函数。这对于简化具有相似参数的重复函数调用非常有用。
示例:幂函数
考虑一个用来求幂的函数。我们可以使用 partial
来创建新的函数,比如平方、立方等。
from functools import partial
def power(base, exponent):
return base ** exponent
# 创建新函数
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
# 使用示例
print(square(4)) # 输出: 16
print(cube(4)) # 输出: 64
上面,partial(power, exponent=2)
创建了一个新函数 square
,其中 exponent
参数被固定为 2;同样地,cube
将 exponent
固定为 3。现在,新函数 square
和 cube
更易于使用和阅读,使得代码更加简洁明了。
需要注意的是,通过 partial
设置的参数是固定的。它们不是默认参数,我们在之前讨论可变默认参数时已经提及过这一话题。
高级用法
partial
还可以用于定制具有多个参数的函数。
from functools import partial
def greet(greeting, name):
return f"{greeting}, {name}!"
# 创建新函数
say_hello = partial(greet, greeting="Hello")
say_hi = partial(greet, greeting="Hi")
# 使用示例
print(say_hello("Alice")) # 输出: Hello, Alice!
print(say_hi("Bob")) # 输出: Hi, Bob!
3. 用于聚合的功能编程:reduce
reduce
函数将一个双元函数累积地应用于可迭代对象的元素,从左到右,从而将可迭代对象缩减为单一值。这对于聚合结果特别有用。
示例:列表求和
让我们使用 reduce
来计算一个数字列表的总和。
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# 对列表求和
total = reduce(lambda x, y: x + y, numbers)
print(total) # 输出: 15
lambda 函数 reduce(lambda x, y: x + y, numbers)
将两个数字相加。因此,reduce
将此函数累积地应用于 numbers
中的元素。列表 [1, 2, 3, 4, 5]
被缩减为总和 15
,即所有元素的和。
高级用法
让我们来看一个更复杂的例子:根据特定键找出字典列表中的最大值。
data = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 25},
{'name': 'Charlie', 'age': 35}
]
# 查找最大年龄
oldest = reduce(lambda x, y: x if x['age'] > y['age'] else y, data)
print(oldest) # 输出: {'name': 'Charlie', 'age': 35}
现在,lambda 函数 reduce(lambda x, y: x if x['age'] > y['age'] else y, data)
比较两个字典中的 age
键,并返回年龄较大的那个字典。因此,reduce
处理列表,并返回年龄最大的字典。
结合 partial
和 reduce
我们可以结合使用 partial
和 reduce
来创建专门的聚合函数。
# 定制的求和聚合函数
sum_partial = partial(reduce, lambda x, y: x + y)
numbers = [1, 2, 3, 4, 5]
print(sum_partial(numbers)) # 输出: 15
# 定制的最大值聚合函数
max_age = partial(reduce, lambda x, y: x if x['age'] > y['age'] else y)
print(max_age(data)) # 输出: {'name': 'Charlie', 'age': 35}
4. 将比较函数转换为键函数:cmp_to_key
cmp_to_key
函数将旧式的比较函数转换为键函数,这种键函数可以在排序函数(如 sorted
和 list.sort()
)中使用。当处理遗留代码或者需要定义自定义排序逻辑时,这特别有用。
示例:使用 cmp_to_key
自定义排序
让我们创建一个自定义排序函数,根据元组的第二个元素来排序一个元组列表。
from functools import cmp_to_key
# 比较函数
def compare_items(a, b):
if a[1] < b[1]:
return -1
elif a[1] > b[1]:
return 1
else:
return 0
# 示例数据:创建一个名为 data 的元组列表,每个元组包含两个元素:
# 一个数字和代表一种水果的字符串。
data = [(1, 'banana'), (2, 'apple'), (3, 'pear')]
# 使用 cmp_to_key 排序
sorted_data = sorted(data, key=cmp_to_key(compare_items))
print(sorted_data) # 输出: [(2, 'apple'), (1, 'banana'), (3, 'pear')]
上面,compare_items(a, b)
根据元组的第二个元素定义了自定义排序逻辑。然后,cmp_to_key
将比较函数转换为可以与 sorted
一起使用的键函数。
我个人一开始觉得这种技巧并不是那么直观。所以,让我们仔细研究一下比较函数和排序技术。
定义比较函数
def compare_items(a, b):
if a[1] < b[1]:
return -1
elif a[1] > b[1]:
return 1
else:
return 0
函数 compare_items
接受两个元组 a
和 b
作为参数,该函数比较这两个元组的第二个元素(即 a[1]
和 b[1]
):
- 如果
a[1]
小于b[1]
,函数返回-1
,表示a
应该排在b
之前。 - 如果
a[1]
大于b[1]
,函数返回1
,表示a
应该排在b
之后。 - 如果
a[1]
等于b[1]
,函数返回0
,表示a
和b
的顺序不需要改变。
排序数据
# 使用 cmp_to_key 排序
sorted_data = sorted(data, key=cmp_to_key(compare_items))
print(sorted_data) # 输出: [(2, 'apple'), (1, 'banana'), (3, 'pear')]
sorted
函数用于对列表 data
进行排序。sorted
的 key
参数设置为 cmp_to_key(compare_items)
。这将 compare_items
比较函数转换为 sorted
可以使用的键函数。
cmp_to_key(compare_items)
: 将compare_items
函数转换为键函数。sorted(data, key=cmp_to_key(compare_items))
: 使用由cmp_to_key
生成的键函数对data
列表进行排序。
排序后的结果存储在 sorted_data
中。
总结
- 比较函数:
compare_items
被定义来比较两个元组的第二个元素。 - 示例数据:创建了一个元组列表(
data
)。 - 排序:使用
sorted
和cmp_to_key(compare_items)
对data
进行排序,得到sorted_data
。 - 输出:打印排序后的列表,显示按水果名称排序的元组。
高级用法
让我们扩展自定义排序来处理更复杂的数据结构,例如根据多个键对字典列表进行排序。
from functools import cmp_to_key
# 多键排序的比较函数
def compare_multi_keys(a, b):
if a['age'] != b['age']:
return a['age'] - b['age']
return (a['name'] > b['name']) - (a['name'] < b['name'])
# 示例数据
data = [
{'name': 'Alice', 'age': 30},
{'name': 'Bob', 'age': 24},
{'name': 'Charlie', 'age': 25},
{'name': 'Dave', 'age': 35}
]
# 使用 cmp_to_key 排序
sorted_data = sorted(data, key=cmp_to_key(compare_multi_keys))
print(sorted_data)
# 输出: [{'name': 'Bob', 'age': 25},
# {'name': 'Charlie', 'age': 25},
# {'name': 'Alice', 'age': 30},
# {'name': 'Dave', 'age': 35}]
compare_multi_keys
函数首先根据 age
排序,然后根据 name
排序,而 cmp_to_key
将多键比较函数转换为排序时使用的键函数。让我们再次仔细观察以便完全理解。
第一次比较:年龄
if a['age'] != b['age']:
return a['age'] - b['age']
这段代码比较字典 a
和 b
中的 'age'
值:
- 如果
'age'
值不相等(即a['age'] != b['age']
),函数返回差值(即a['age'] - b['age']
)。
返回差值确保通过结果的符号(即正或负)来进行排序。符号表示:
- 如果
a['age']
小于b['age']
,则返回负值,表明排序时a
应该排在b
之前。 - 如果
a['age']
大于b['age']
,则返回正值,表明排序时a
应该排在b
之后。
这样就保证了字典主要按照年龄进行排序。
第二次比较:姓名
return (a['name'] > b['name']) - (a['name'] < b['name'])
如果年龄相同,则这一行比较字典 a
和 b
中的 'name'
值:
- 表达式
(a['name'] > b['name'])
如果a['name']
字典顺序大于b['name']
则评估为True
(1),否则评估为False
(0)。 - 表达式
(a['name'] < b['name'])
如果a['name']
字典顺序小于b['name']
则评估为True
(1),否则评估为False
(0)。 - 两个布尔结果相减给出:
1 - 0 = 1
如果a['name']
大于b['name']
,表明排序时a
应该排在b
之后。0 - 1 = -1
如果a['name']
小于b['name']
,表明排序时a
应该排在b
之前。0 - 0 = 0
如果a['name']
等于b['name']
。
总结
函数 compare_multi_keys
执行两层比较:
- 首先根据
'age'
键对字典进行排序。 - 如果两个字典的
'age'
相同,则根据'name'
键对它们进行排序。
因此,我们的函数可以与接受比较函数的排序函数一起使用(即 sorted
与 functools.cmp_to_key
来实现多键排序)。
结论
Python 中的 functools
模块是一颗隐藏的瑰宝,提供了强大的工具来优化和简化源代码。它的 lru_cache
、partial
、reduce
和 cmp_to_key
使 Python 项目更加高效、易读和便于维护。functools
的高级技术具有双重优势:(1)提高性能,(2)增强清晰度和表达力。
无论是处理性能瓶颈、函数调用中的重复问题,还是复杂的排序,functools
都是你的得力助手。试试看吧!将 functools
应用于 Python 项目中。