【问题标题】:Fastest method for extracting sub-list from Python list given array of indexes从给定索引数组的Python列表中提取子列表的最快方法
【发布时间】:2021-01-18 17:10:18
【问题描述】:

我有任何类型的对象的大型 Python 列表 l,我还有另一个大型列表 i(甚至是 NumPy 数组)指向列表 l 中的某些元素的整数索引。

问题是创建另一个列表l2 的最快(最有效)方法是什么,该列表包含l 的元素,索引来自i

最简单的方法是做一个列表推导:

l2 = [l[si] for si in i]
# Use np.nditer(i) instead of i, for NumPy array case

但这是最快的方法吗?

列表理解是一个 Python 循环,所以对于大型列表可能会很慢,也许标准库中有一些内置的 Python 方法,用高效的C 编写来完成这个任务?或者NumPy有这样的方法可以通过numpy数组索引Python的列表?

也许标准python库中有一些简单而快速的函数可以对NumPy的np.take做同样的事情,就像下面的假想代码:

import listtools
l2 = listtools.take(l, indexes)

【问题讨论】:

  • 这里回答的问题完全相同 - stackoverflow.com/questions/9008533/…。那里的建议是使用生成器而不是列表理解
  • @YossiLevi 我不相信生成器是最快的方法,同样对于我的任务,您需要通过执行 l2 = list(generator_()) 将列表从生成器中取出,这是另一个缓慢的操作。同样在那个问题中只有一个答案,我希望还有十几种其他没有提到的快速方法。
  • 列表一次只能索引一项(4或切片)。并且迭代列表比迭代数组更快(nditer 没有帮助)。所以基本的列表理解是最有意义的。请记住,它只是复制参考。
  • @hpaulj 我想为大型列表找到最快的方法。我担心的是列表理解本身很慢,因为它是一个 python 循环代码。所以我认为标准库中可能有一些快速简单的特殊方法,例如喜欢import listtools; l2 = listtools.take(l, indexes)
  • @Arty。列表推导比通用循环快很多,Paul 的回答显示最接近的 python 必须是 take

标签: python arrays list numpy


【解决方案1】:

通过使用支持批量查找的operator.itemgetter,您可以获得较小的加速(在下面的示例中约为 25%):

>>> import string
>>> import random
>>> import operator as op
>>> from timeit import timeit

# create random lists
>>> l = [random.choice([*string.ascii_letters,*range(100)]) for _ in range(1000000)]
>>> i = [random.randint(0,999999) for _ in range(300000)]

# timings
>>> timeit(lambda:[l[si] for si in i],number=100)
3.0997245000035036
>>> timeit(lambda:list(map(l.__getitem__,i)),number=100)
2.892384369013598
>>> timeit(lambda:list(op.itemgetter(*i)(l)),number=100)
2.1787672539940104

【讨论】:

  • 谢谢!有趣的是,对于索引为 NumPy 数组的情况,是否还有一些改进的解决方案?
  • @Arty numpy 和纯 python 不能很好地混合,所以在这种情况下,我能想到的最好的方法是使用 i.tolist() 将索引实际转换为列表。这会更快,但显然不如从 getgo 获得索引列表那么快。
  • 我决定测量 numpy 的使用情况,因为当我们将索引作为 numpy 数组时,我也有兴趣解决任务,在这种情况下,我们获得 1.56x 加速,如果 l 也是一个 numpy 对象数组,然后我们获得 3x 加速。 Here is my answer.
  • @Arty 有趣。实际上,给予或不给予“dtype”kw 有很大的不同,特别是如果 numpy 决定它不需要object dtype,因为它可以在任何地方使用字符串。这使得最终的tolist 更加昂贵。但是,如果区别只是numpy 必须自己找出它必须使用object,这会花费大量额外的时间。
  • 是的,最好总是提供dtype,因为在最坏的情况下,numpy 必须扫描整个数组以找出可以存储任何类型数组值的最小类型。然后转换将慢两倍。提供最窄的类型也总是好的,如果你有整数做np.int64如果也浮动然后np.float64如果只有字符串然后np.str_如果有任何对象然后np.object_。最窄的类型将为所有 numpy 算法提供最佳效率。
【解决方案2】:

已知NumPy数组也可以通过dtype = np.object_来存储和处理任意Python对象。

所以我决定测量 NumPy 与普通 python 相比的使用速度。另外,正如我在问题中提到的,我还想解决索引是 numpy 整数数组的情况。

接下来的代码测量不同的情况,我们是否需要将源列表转换为 numpy 数组以及是否也应该转换结果。

Try it online!

import string
from timeit import timeit
import numpy as np
np.random.seed(0)

letters = np.array(list(string.ascii_letters), dtype = np.object_)
nl = letters[np.random.randint(0, len(letters), size = (10 ** 6,))]
l = nl.tolist()
ni = np.random.permutation(np.arange(nl.size, dtype = np.int64))
i = ni.tolist()

pyt = timeit(lambda: [l[si] for si in i], number = 10)
print('python:', round(pyt, 3), flush = True)

for l_from_list in [True, False]:
    for i_from_list in [True, False]:
        for l_to_list in [True, False]:
            def Do():
                cl = np.array(l, dtype = np.object_) if l_from_list else nl
                ci = np.array(i, dtype = np.int64) if i_from_list else ni
                res = cl[ci]
                res = res.tolist() if l_to_list else res
                return res
            ct = timeit(lambda: Do(), number = 10)
            print(
                'numpy:', 'l_from_list', l_from_list, 'i_from_list', i_from_list, 'l_to_list', l_to_list,
                'time', round(ct, 3), 'speedup', round(pyt / ct, 2), flush = True
            )

输出:

python: 2.279
numpy: l_from_list True  i_from_list True  l_to_list True  time 2.924 speedup 0.78
numpy: l_from_list True  i_from_list True  l_to_list False time 2.805 speedup 0.81
numpy: l_from_list True  i_from_list False l_to_list True  time 1.457 speedup 1.56
numpy: l_from_list True  i_from_list False l_to_list False time 1.312 speedup 1.74
numpy: l_from_list False i_from_list True  l_to_list True  time 2.352 speedup 0.97
numpy: l_from_list False i_from_list True  l_to_list False time 2.209 speedup 1.03
numpy: l_from_list False i_from_list False l_to_list True  time 0.894 speedup 2.55
numpy: l_from_list False i_from_list False l_to_list False time 0.75  speedup 3.04

所以我们可以看到,如果我们将所有列表存储为 numpy 数组,那么我们将获得 3x 加速!但是,如果只有索引是一个 numpy 数组,那么我们的加速比只有 1.56x,这也非常好。在所有内容都必须从列表中来回转换的情况下,我们获得了0.78x 的加速,这意味着我们减慢了速度,因此如果我们只使用列表而不是通过 numpy 进行索引是没有帮助的。

【讨论】:

  • 几件事:不需要lambda: Do():你可以使用Do(没有括号)。 Do 本身正在做很多额外的工作,决定和复制它可能不应该做的事情。建议:使用timeit 接受一串以分号分隔的语句来构建命令而不是在运行时进行决策。
  • 顺便说一句,那些都是挑剔的。 +1 无论如何
  • 还有两件事:为了公平起见,请使用 operator.itemgerter 作为其他答案的建议,并尝试几种不同尺寸的基准,例如 100、1k、10k、100k 左右跨度>
  • 像这样的索引是对象 dtype 数组胜过列表的一个领域。索引不关心元素代表什么,它们的 dtype。它只需要注意步幅,即它必须在每个维度中步进的字节数。
  • @MadPhysicist 我认为在通过timeit 衡量的任何函数内部,只要他们使用非常少量的整个函数运行,就可以做出任何if 决定。特别是在代码变得更容易和可读的情况下。当然,一些不会改变被计时函数行为的重计算代码应该放在外面的某个地方,比如 timeit 的初始化参数。
猜你喜欢
  • 1970-01-01
  • 2015-02-05
  • 1970-01-01
  • 1970-01-01
  • 2014-04-20
  • 2011-08-25
  • 1970-01-01
  • 1970-01-01
  • 2021-06-15
相关资源
最近更新 更多