小能豆

为什么“any()”比使用循环运行得更慢?

py

我一直在一个管理大量单词列表的项目上工作,并通过大量测试来验证列表中的每个单词。有趣的是,每次我使用“更快”的工具(如模块)时itertools,它们似乎都变慢了。

最后我决定提出这个问题,因为我可能做错了什么。以下代码将尝试测试函数any()与循环使用的性能。

#!/usr/bin/python3
#

import time
from unicodedata import normalize


file_path='./tests'


start=time.time()
with open(file_path, encoding='utf-8', mode='rt') as f:
    tests_list=f.read()
print('File reading done in {} seconds'.format(time.time() - start))

start=time.time()
tests_list=[line.strip() for line in normalize('NFC',tests_list).splitlines()]
print('String formalization, and list strip done in {} seconds'.format(time.time()-start))
print('{} strings'.format(len(tests_list)))


unallowed_combinations=['ab','ac','ad','ae','af','ag','ah','ai','af','ax',
                        'ae','rt','rz','bt','du','iz','ip','uy','io','ik',
                        'il','iw','ww','wp']


def combination_is_valid(string):
    if any(combination in string for combination in unallowed_combinations):
        return False

    return True


def combination_is_valid2(string):
    for combination in unallowed_combinations:
        if combination in string:
            return False

    return True


print('Testing the performance of any()')

start=time.time()
for string in tests_list:
    combination_is_valid(string)
print('combination_is_valid ended in {} seconds'.format(time.time()-start))


start=time.time()
for string in tests_list:
    combination_is_valid2(string)
print('combination_is_valid2 ended in {} seconds'.format(time.time()-start))  

前面的代码非常代表我所做的测试类型,如果我们看一下结果:

File reading done in 0.22988605499267578 seconds
String formalization, and list strip done in 6.803032875061035 seconds
38709922 strings
Testing the performance of any()
combination_is_valid ended in 80.74802565574646 seconds
combination_is_valid2 ended in 41.69514226913452 seconds


File reading done in 0.24268722534179688 seconds
String formalization, and list strip done in 6.720442771911621 seconds
38709922 strings
Testing the performance of any()
combination_is_valid ended in 79.05265760421753 seconds
combination_is_valid2 ended in 42.24800777435303 seconds

我发现使用循环比使用 快一半,这有点令人惊奇any()。这该如何解释?我做错了什么吗?

(我在GNU-Linux下使用了python3.4)


阅读 15

收藏
2024-11-12

共1个答案

小能豆

any()此处函数与传统循环之间的速度差异for可能源于 中的生成器表达式引入的开销any()。下面详细介绍了为什么any()在这种特定情况下速度可能会变慢,以及一些提高性能的建议。

开销解释any()

带有生成器表达式的函数any()(如 中使用combination_is_valid)会引入额外的函数调用并创建一个可迭代对象,这会增加开销。生成器表达式(combination in string for combination in unallowed_combinations)会创建一个迭代器并依次检查每个项目,直到找到一个True值。每次combination in string检查都涉及一个函数调用,这会使此设置在您处理非常大的组合列表和高迭代次数的情况下变慢。

另一方面,传统的for循环combination_is_valid2避免创建生成器,而是直接迭代unallowed_combinations。这种方法的开销较小,因为 Python 可以直接处理循环,并且不需要创建中间生成器对象或管理生成器状态,从而减少函数调用。

分析代码

为了确定瓶颈是否确实是生成器表达式,并分析是否存在潜在的优化,您可以尝试使用模块进行分析cProfile

import cProfile
cProfile.run("for string in tests_list: combination_is_valid(string)")
cProfile.run("for string in tests_list: combination_is_valid2(string)")

这将让你了解时间都花在了哪里。

优化combination_is_valid

如果您希望加快该combination_is_valid功能,请采用以下一些策略:

  1. 使用集合进行查找(如果可能):如果不允许的组合数量很大,则将它们转换为集合可以提高速度。但是,这仅在检查整个单词组合而不是子字符串时才有效。

  2. 优化in检查:如果组合列表很大,您可以unallowed_combinations按每个字符串的长度排序,首先检查最长的字符串,从而加快子字符串搜索速度。这是因为较短的字符串更有可能作为子字符串出现,这可能会减少对长字符串的检查次数。

  3. 减少生成器表达式开销:您可以通过内联编写循环而不是使用combination_is_valid来更接近:combination_is_valid2``any()

def combination_is_valid(string): for combination in unallowed_combinations: if combination in string: return False return True

  1. 尝试使用re.compile(如果需要正则表达式模式)进行编译:如果组合是复杂模式而不是固定子字符串,请考虑将它们编译为正则表达式模式。

列表推导的略微不同方法

如果您想避免使用生成器表达式但仍利用any(),请尝试在内使用列表推导any()

def combination_is_valid(string):
    return not any([combination in string for combination in unallowed_combinations])

这种方法的性能可能比使用生成器表达式稍好一些,因为它不需要创建迭代器;然而,它的性能可能仍然不及普通for循环。

为什么在某些情况下循环更快

传统循环通常对高度重复的任务(如您的情况)更快的原因是 Python 可以比复杂表达式更有效地优化简单循环,尤其是在不需要中间对象(如生成器)时。通过最小化函数调用和中间状态,for循环通常可以在高迭代场景中提供更好的性能。

概括

您没有做错什么,但是 Python 的any()带有生成器表达式的函数确实会因为额外的函数调用和迭代器管理而带来少量开销。这种开销在性能至关重要、重复性强且数据量大的任务(例如您的任务)中会变得非常明显。

2024-11-12