在编写 Python 代码时,使用循环并不一定是一种坏的设计模式,但过多的循环会导致效率低下且浪费资源。今天我们来探讨一些工具,帮助我们在代码中消除不必要的循环。Python 提供了几种循环模式,当我们需要遍历一个对象的内容时,可以使用:
- for 循环:逐个遍历序列中的元素。
- while 循环:只要满足某个布尔条件,就不断重复执行。
- 嵌套循环:在一个循环内使用多个循环。
虽然 Python 支持这些循环模式,但我们在使用时要格外小心。因为大多数循环都是逐个元素进行运算,这通常会导致效率较低。
在编写高效代码时,我们应尽量避免使用循环。消除循环通常会使代码行数减少,且更易于理解。Python 的一条惯用语是“平坦比嵌套更好”,我们努力消除代码中的循环,有助于遵循这一惯用法。本文将讨论并探索几种简单的技术,帮助我们消除循环,或至少以更好的方式实现它们。
1. 消除循环
1.1 使用列表推导式、map 函数和 itertools 消除循环
假设我们有一个列表的列表,称为 poke_stats
,其中包含每个宝可梦的统计数据。每一行对应一只宝可梦,每一列代表宝可梦的特定统计值。这里,列表示宝可梦的生命值、攻击、防御和速度。我们希望对每一行进行求和,得到每只宝可梦的总统计数据。如果使用循环来计算每行的和,我们需要遍历每一行,并将该行的和添加到总和列表中。但我们可以通过列表推导式来实现相同的功能,代码行数更少,效率也更高。或者,我们还可以使用之前在我的文章《为数据科学家编写高效的 Python 代码》中介绍过的内建 map 函数。
首先,我们定义一个统计数组:
poke_stats = [[45, 49, 49, 45], [60, 62, 63, 60], [80, 82, 83, 80]] # 示例数据
然后我们可以使用 for
循环来遍历这些元素并打印运行时间:
import time
start_time = time.time()
totals = []
for row in poke_stats:
totals.append(sum(row))
end_time = time.time()
print("Using loop:", end_time - start_time)
接着,我们可以用列表推导式来简化这段代码:
start_time = time.time()
totals = [sum(row) for row in poke_stats]
end_time = time.time()
print("Using list comprehension:", end_time - start_time)
最后,我们使用 map
函数来完成同样的操作:
start_time = time.time()
totals = list(map(sum, poke_stats))
end_time = time.time()
print("Using map function:", end_time - start_time)
这三种方法都可以得到相同的结果,但使用列表推导式或 map
函数的代码行数更少,执行速度也更快。
在之前的文章中,我们还介绍了一些内建模块,这些模块可以帮助我们消除循环。例如,我们可以使用 itertools
模块中的 combinations
,替代嵌套的 for
循环,从而获得更简洁、高效的解决方案。
1.2 使用 NumPy 消除循环
另一个强大的消除循环的技术是使用 NumPy 包。假设我们有一个包含统计数据的 NumPy 数组,而不是列表的列表。
我们希望收集每只宝可梦(或每一行)的平均统计值。可以使用循环遍历数组并收集行的平均值:
import numpy as np
poke_stats_array = np.array([[45, 49, 49, 45], [60, 62, 63, 60], [80, 82, 83, 80]]) # 示例数据
averages = []
for row in poke_stats_array:
averages.append(np.mean(row))
然而,NumPy 数组允许我们一次性对整个数组进行计算。我们可以使用 .mean
方法并指定轴 axis=1
来计算每行的平均值(即沿列方向计算平均值)。这样,我们就不再需要使用循环,效率也更高。
start_time = time.time()
averages = poke_stats_array.mean(axis=1)
end_time = time.time()
print("Using NumPy mean:", end_time - start_time)
通过比较运行时间,我们可以看到,使用 .mean
方法直接计算整个数组的均值,效率比使用循环要高得多。
2. 编写更高效的循环
我们已经讨论了如何消除循环,但有时循环是不可避免的。在本节中,我们将探讨如何在循环不可避免的情况下提高循环效率。我们将假设下面的示例是无法消除的循环用例,目的是帮助大家优化现有的循环。
优化循环的最佳方式是分析循环内部的操作,确保每次迭代不会做不必要的工作。如果一个计算在每次循环中都执行,但其值并未发生变化,那么最好将此计算移到循环外部。比如,在每次迭代时进行的数据类型转换,如果转换的结果不变,也可以使用 map
函数在循环外部完成。任何可以一次性完成的工作都应该移出循环。让我们看一些示例。
2.1 将计算移到循环外部
假设我们有一个宝可梦名称列表和一个对应的攻击值数组。我们希望打印出攻击值大于所有攻击值平均值的宝可梦名称。为了实现这个目标,我们可以使用一个循环,遍历每个宝可梦及其攻击值。在每次迭代时,我们都计算所有攻击值的平均值。然后,检查每只宝可梦的攻击值是否超过这个平均值。
这种做法的低效之处在于,total_attack_avg
变量在每次循环中都被重新计算。其实,total_attack_avg
是一个常量,不会随着迭代而改变,我们只需要计算一次。将计算移到循环外部,我们就可以提高效率。
total_attack_avg = np.mean(attack_values) # 移到循环外部
for name, attack in zip(pokemon_names, attack_values):
if attack > total_attack_avg:
print(name)
通过将计算移到循环外部,我们可以减少不必要的操作,提升效率。
2.2 整体转换
另一种提高循环效率的方法是使用整体转换。在下面的例子中,我们有三个列表:宝可梦的名称列表、是否为传说宝可梦的布尔值列表、宝可梦的代际列表。
我们希望将这些列表组合成一个包含每只宝可梦名称、状态和代际的列表。为此,我们使用 zip
函数将这些列表组合成元组,然后遍历并将每个元组转换为列表。
poke_names = ['Pikachu', 'Charmander', 'Bulbasaur']
is_legendary = [False, False, False]
generations = [1, 1, 1]
poke_data = []
for name, legendary, generation in zip(poke_names, is_legendary, generations):
poke_data.append([name, legendary, generation])
然而,转换每个元组为列表的做法并不高效。我们可以先将所有元组收集到一起,然后使用 map
函数一次性转换所有元组,而不需要在每次循环中进行转换。
poke_tuples = list(zip(poke_names, is_legendary, generations))
poke_data = list(map(list, poke_tuples)) # 整体转换
通过将类型转换移到循环外部,我们可以显著提高效率。
在编写 Python 代码时,尽量减少或消除循环的使用,可以使代码更简洁、更高效。在循环不可避免的情况下,我们也可以通过分析和优化循环中的操作,提高代码的性能。通过使用列表推导式、map 函数、NumPy 和 itertools 等工具,我们可以轻松地消除不必要的循环,从而提高代码效率。