一个我希望早些知道的Python模块:Functools如何提高工作效率

引言

在探索编写高效 Python 代码的旅程中,我们的下一站是 functools 模块。正如作者们所描述的那样:

functools 模块包含了高级函数,这些函数作用于其他函数或是返回其他函数。通常情况下,任何可调用的对象都可以作为该模块的目的来处理。

这个模块里包含了能够显著提升 Python 项目性能、可读性和灵活性的强大工具。在这篇博客中,我们将探讨这个模块中的四个方面,它们可能会改变你对下一个项目的开发方式。具体来说,我们来学习一下 lru_cachepartialreducecmp_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;同样地,cubeexponent 固定为 3。现在,新函数 squarecube 更易于使用和阅读,使得代码更加简洁明了。

需要注意的是,通过 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 处理列表,并返回年龄最大的字典。

结合 partialreduce

我们可以结合使用 partialreduce 来创建专门的聚合函数。

# 定制的求和聚合函数
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 函数将旧式的比较函数转换为键函数,这种键函数可以在排序函数(如 sortedlist.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 接受两个元组 ab 作为参数,该函数比较这两个元组的第二个元素(即 a[1]b[1]):

  • 如果 a[1] 小于 b[1],函数返回 -1,表示 a 应该排在 b 之前。
  • 如果 a[1] 大于 b[1],函数返回 1,表示 a 应该排在 b 之后。
  • 如果 a[1] 等于 b[1],函数返回 0,表示 ab 的顺序不需要改变。

排序数据

# 使用 cmp_to_key 排序
sorted_data = sorted(data, key=cmp_to_key(compare_items))
print(sorted_data)  # 输出: [(2, 'apple'), (1, 'banana'), (3, 'pear')]

sorted 函数用于对列表 data 进行排序。sortedkey 参数设置为 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)。
  • 排序:使用 sortedcmp_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']

这段代码比较字典 ab 中的 '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'])

如果年龄相同,则这一行比较字典 ab 中的 '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 执行两层比较:

  1. 首先根据 'age' 键对字典进行排序。
  2. 如果两个字典的 'age' 相同,则根据 'name' 键对它们进行排序。

因此,我们的函数可以与接受比较函数的排序函数一起使用(即 sortedfunctools.cmp_to_key 来实现多键排序)。

结论

Python 中的 functools 模块是一颗隐藏的瑰宝,提供了强大的工具来优化和简化源代码。它的 lru_cachepartialreducecmp_to_key 使 Python 项目更加高效、易读和便于维护。functools 的高级技术具有双重优势:(1)提高性能,(2)增强清晰度和表达力。

无论是处理性能瓶颈、函数调用中的重复问题,还是复杂的排序,functools 都是你的得力助手。试试看吧!将 functools 应用于 Python 项目中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值