【问题标题】:Fastest possible way to iterate through a specific list?迭代特定列表的最快方法?
【发布时间】:2013-10-01 12:03:40
【问题描述】:

假设我有一个列表:

list=['plu;ean;price;quantity','plu1;ean1;price1;quantity1']

我想遍历列表 + 用“;”分割列表并添加一个 if 子句,如下所示:

for item in list:
    split_item=item.split(";")
    if split_item[0] == "string_value" or split_item[1] == "string_value":
        do something.....

我想知道,这是否是最快的方法?假设我的初始列表更大(有更多列表项)。我尝试了列表推导:

item=[item.split(";") for item in list if item.split(";")[0] == "string_value" or item.split(";")[1] == "string_value"]

但这实际上给了我更慢的结果。第一种情况给我平均 90 毫秒,而第二种情况给我平均 130 毫秒。 我做列表理解错了吗?有更快的解决方案吗?

【问题讨论】:

  • 在第一种情况下,您调用item.split(";") 一次。在第二种情况下,您拨打item.split(";") 3 次。第二种情况肯定会慢一些。
  • 旁白:如果您正在处理表格数据,可能值得研究像pandas 这样的库。当/如果你可以矢量化操作,你可以获得比纯 Python 循环显着的性能优势,当然这完全取决于你没有提到的问题的部分。

标签: python performance list list-comprehension


【解决方案1】:

编辑:事实证明,正则表达式缓存对竞争有点不公平。我的错。正则表达式只快了一小部分。

如果您正在寻找速度,hcwhsa 的答案应该足够好。如果您需要更多,请查看re

import re
from itertools import chain

lis = ['plu;ean;price;quantity'*1000, 'plu1;ean1;price1;quantity1'*100]*1000

matcher = re.compile('^(?:plu(?:;|$)|[^;]*;ean(?:;|$))').match
[l.split(';') for l in lis if matcher(l)]

时间安排,主要是积极的结果(又名。split 是缓慢的主要原因):

SETUP="
import re
from itertools import chain
matcher = re.compile('^(?:plu(?:;|$)|[^;]*;ean(?:;|$))').match

lis = ['plu1;ean1;price1;quantity1'+chr(i) for i in range(10000)] + ['plu;ean;price;quantity' for i in range(10000)]
"

python -m timeit -s "$SETUP" "[[x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== 'plu' or y=='ean']"
python -m timeit -s "$SETUP" "[l.split(';') for l in lis if matcher(l)]"

我们看到我的快一点。

10 loops, best of 3: 55 msec per loop
10 loops, best of 3: 49.5 msec per loop

对于大多数负面结果(大多数东西都被过滤了):

SETUP="
import re
from itertools import chain
matcher = re.compile('^(?:plu(?:;|$)|[^;]*;ean(?:;|$))').match

lis = ['plu1;ean1;price1;quantity1'+chr(i) for i in range(1000)] + ['plu;ean;price;quantity' for i in range(10000)]
"

python -m timeit -s "$SETUP" "[[x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== 'plu' or y=='ean']"
python -m timeit -s "$SETUP" "[l.split(';') for l in lis if matcher(l)]"

领先者触摸更高。

10 loops, best of 3: 40.9 msec per loop
10 loops, best of 3: 35.7 msec per loop

如果结果总是唯一的,使用

next([x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== 'plu' or y=='ean')

或更快的正则表达式版本

next(filter(matcher, lis)).split(';')

(在 Python 2 上使用 itertools.ifilter)。

时间安排:

SETUP="
import re
from itertools import chain
matcher = re.compile('^(?:plu(?:;|$)|[^;]*;ean(?:;|$))').match

lis = ['plu1;ean1;price1;quantity1'+chr(i) for i in range(10000)] + ['plu;ean;price;quantity'] + ['plu1;ean1;price1;quantity1'+chr(i) for i in range(10000)]
"

python -m timeit -s "$SETUP" "[[x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== 'plu' or y=='ean']"
python -m timeit -s "$SETUP" "next([x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== 'plu' or y=='ean')"

python -m timeit -s "$SETUP" "[l.split(';') for l in lis if matcher(l)]"
python -m timeit -s "$SETUP" "next(filter(matcher, lis)).split(';')"

结果:

10 loops, best of 3: 31.3 msec per loop
100 loops, best of 3: 15.2 msec per loop
10 loops, best of 3: 28.8 msec per loop
100 loops, best of 3: 14.1 msec per loop

因此,这极大地促进了这两种方法。

【讨论】:

  • 哇,正则表达式不是我的强项,但这将其缩短到 40 毫秒。因为在我的情况下,除了 1 之外的所有东西都被过滤了,这是迄今为止最好的解决方案
  • 我认为您不接受这一点是对的吗?如果是这样,为什么?另外,我有一个新方法,对于独特的解决方案,无论有没有正则表达式,它都应该更快。
  • 原来正则表达式缓存是不公平的。无论哪种方式,您都可能想重新查看我的答案的最后一部分。
  • 嘿伙计,我不知道为什么这不被接受。也许我做错了。到目前为止,您的解决方案对我来说是最好的。由于我正在将我的代码移植到 Windows 移动设备(使用 python 2.5 的 PyCe 端口),这就是我需要性能的原因:)。再次感谢。
  • 因为 itertools 不包含在 python 2.5 中,所以我使用了这个,它给了我迄今为止最好的结果:[next(l.split(';') for l in list if matcher(l))]。我尝试了在 stackoverflow 上找到的多种解决方案,但目前这给了我最好的结果。
【解决方案2】:

我想知道,这是否是最快的方法?

不,当然不是。与 Python 相比,您可以在手工编码的汇编中更快地实现它。那又怎样?

如果“做某事...”不是微不足道的,并且有很多匹配项,那么做某事 100000 次的成本将比循环 500000 次的成本贵很多,所以找到最快的方法循环根本不重要。

事实上,只在每个循环中调用split 两到三个而不是记住和重用结果将会淹没迭代的成本,并且当你只关心两个结果时不传递 maxsplit 参数也可以。


所以,您正在尝试优化错误的东西。但是,如果在你修复了所有其他问题之后,结果证明迭代成本在这里真的很重要呢?

嗯,你不能直接使用推导来加快速度,因为推导是用于返回值的表达式,而不是用于执行操作的语句。

但是,如果您查看您的代码,您会意识到您实际上是在做三件事:拆分每个字符串,然后过滤掉不匹配的字符串,然后执行“做某事”。因此,您可以对前两部分使用推导式,然后您只对通过过滤器的更小的值列表使用慢速 for 循环。

看起来你尝试了这个,但你犯了两个错误。

首先,您最好使用生成器表达式而不是列表推导式 - 您在这里不需要列表,只需要迭代的东西,所以不要花钱构建一个。

第二,你不想split字符串三次。您可能会找到一些复杂的方法来让split 在一次理解中完成一次,但是为什么要麻烦呢?只需将每个步骤写成自己的步骤即可。

所以:

split_items = (item.split(';') for item in items)
filtered_items = (item for item in split_items 
                  if item[0] == "string_value" or item[1] == "string_value")
for item in filtered_items:
    do something...

这实际上会更快吗?如果您可以获得一些真实的测试数据和“做某事......”代码,这表明迭代是一个瓶颈,您可以在这些真实数据和代码上进行测试。在那之前,没有什么可以测试的。

【讨论】:

  • filtered_items =(如果 item[0] == "string_value" 或 item[1] == "string_value",则为 split_items 中的项目的项目)
  • 它给出的结果与我的第一个案例相同,但你让我大开眼界:)
  • @sale1902:感谢您的关注。
【解决方案3】:

我在这里找到了一个不错的选择。

您可以结合使用地图和过滤器。试试这个:

>>>import itertools
>>>splited_list = itertools.imap(lambda x: x.split(";"), your_list)
>>>result = filter(lambda x: filter(lambda x: x[0] == "plu" or x[1] == "string_value", lista)

第一项将创建元素的迭代器。第二个将过滤它。 我在 IPython Notebook shell 中运行了一个小型基准测试,得到了以下结果:

第一次测试:

尺寸小,单线解决方案效果更好

第二次测试:

列表越大,地图/过滤器解决方案略好

第三次测试:

有了大列表和更大的元素,地图/过滤器解决方案就更好了。

我猜随着列表大小的增加,性能差异会继续增加,直到多出 66% 的时间达到峰值(在 10000 个元素的列表试验中)。

map/filter 解决方案和列表理解解决方案之间的区别在于对 .split() 的调用次数。 Ones 为每个项目调用 3 次,另一个只调用一次,因为列表推导式只是一种将映射/过滤器结合在一起的 Python 方式。我过去经常使用列表推导,并认为我不知道 lambda 是什么。直到我发现地图和列表推导是一回事。

如果你不关心内存使用情况,你可以使用常规地图而不是 imap。它将立即创建带有拆分的列表。它将使用更多的内存来存储它,但它会稍微快一些。

实际上,如果您不关心内存使用情况,您可以使用 2 个列表推导式编写映射/过滤器解决方案,并获得相同的确切结果。结帐:

【讨论】:

  • imapifilter 与生成器表达式做同样的事情,只是它们采用 函数 而不是表达式。同样适用于mapfilter 与列表推导。如果您的表达式只是“调用此函数”,那不是问题;实际上,这是一个好处。但是如果您的表达式是 x[0] == "plu" or x[1] == "string_value",则将其包装在函数中会减慢速度(因为每次通过循环),并且还降低了可读性(因为lambda x: 只是妨碍了)。
  • 但是你绝对可以在惰性 (imap/ifilter/genexpr) 和严格 (map/filter/listcomp) 之间取得平衡。如果您要过滤以将 M 值排除在 N 之外,那么在严格变体中浪费的 M 个列表构建步骤的成本可能很容易因为不必经过惰性变体中的 N 个迭代器协议步骤而被抵消,所以filter 或者如果 M 的话,listcomp 通常会获胜
  • 最后,测试时要非常小心。如果您的最后一步只是构建一个geneexpr/imap/ifilter 而您从未对其进行迭代,那么您还没有做任何事情!确保在您的timeit 中循环它(或者,为了尽可能少的测试开销,将其提供给collections.deque(max_len=0))。
  • 但我将 imap 生成器传递给过滤器。遍历整个生成器就够了,不是吗?
  • 是的,只要最后一步严格,一切都好。而且我不认为你只是偶然地做到了这一点。我认为这是有意为之,但想向其他读者(尤其是那些习惯 Python 3 的读者,mapfilter 很懒)说清楚。
【解决方案4】:

仅当从str.split(';', 2)检索到的前两项满足条件时才拆分整个字符串:

>>> strs = 'plu;ean;price;quantity'
>>> strs.split(';', 2)
['plu', 'ean', 'price;quantity']

这里只有前两项满足条件才拆分第三项('price;quantity'):

>>> lis = ['plu;ean;price;quantity'*1000, 'plu1;ean1;price1;quantity1'*1000]*1000

普通的 for 循环,列表中每个项目的整个字符串的单个拆分。

>>> %%timeit
for item in lis:
    split_item=item.split(";")
    if split_item[0] == "plu" or split_item[1] == "ean":pass
... 
1 loops, best of 3: 952 ms per loop

列表推导相当于上面的 for 循环:

>>> %timeit [x for x in (item.split(';') for item in lis) if x[0]== "plu" or x[1]=="ean"]
1 loops, best of 3: 961 ms per loop

按需拆分:

>>> %timeit [[x] + [y] + z.split(';') for x, y, z in (item.split(';', 2) for item in lis) if x== "plu" or y=="ean"]
1 loops, best of 3: 508 ms per loop

当然,如果列表和字符串都很小,那么这样的优化就无所谓了。

【讨论】:

  • 我同意这是这里的问题:如果您只需要前两个元素,则不要拆分所有列表。
  • 这里的优势完全取决于mix。如果“几乎总是” if 条件成功的情况,您应该只进行完全拆分;如果 if 条件“几乎从不”成功,那么您应该首先拆分前两个元素并测试它们。交叉发生的位置取决于列表的平均长度以及部分拆分与完整拆分相比需要多长时间。
  • 这是最好的解决方案,至少对我而言,它在我的测试列表中降至 80 毫秒。由于我的结果(来自 if 条件)将始终是唯一的,因此这是目前这种情况下的最佳解决方案。
猜你喜欢
  • 2022-06-13
  • 2019-04-08
  • 2015-03-07
  • 2021-09-12
  • 2011-04-16
  • 2010-11-08
  • 2023-04-11
  • 2016-08-29
  • 2010-09-16
相关资源
最近更新 更多