TL;DR:set() 函数使用 Python 迭代协议创建一个集合。但是在 NumPy 数组上迭代(在 Python 级别上)非常慢,以至于在进行迭代之前使用 tolist() 将数组转换为 Python 列表会(快得多)。
要了解迭代 NumPy 数组为何如此缓慢,了解 Python 对象、Python 列表和 NumPy 数组是如何存储在内存中的非常重要。
Python 对象需要一些簿记属性(如引用计数、指向其类的链接……)和它所代表的值。例如整数ten = 10 可能如下所示:
蓝色圆圈是您在 Python 解释器中为变量 ten 使用的“名称”,而较低的对象(实例)是实际代表整数的对象(因为这里的簿记属性并不重要,我在图片)。
Python list 只是 Python 对象的集合,例如 mylist = [1, 2, 3] 会这样保存:
这次列表引用了 Python 整数 1、2 和 3,而名称 mylist 仅引用了 list 实例。
但数组myarray = np.array([1, 2, 3]) 不会将 Python 对象存储为元素:
值 1、2 和 3 直接存储在 NumPy array 实例中。
有了这些信息,我可以解释为什么迭代 array 比迭代 list 慢得多:
每次访问list 中的下一个元素时,list 只会返回一个存储的对象。这非常快,因为该元素已经作为 Python 对象存在(它只需要将引用计数加一)。
另一方面,当您想要 array 的元素时,它需要在返回之前为包含所有簿记内容的值创建一个新的 Python“框”。当您遍历数组时,它需要为数组中的每个元素创建一个 Python 框:
创建这些盒子很慢,这是迭代 NumPy 数组比迭代存储值及其盒子的 Python 集合(列表/元组/集合/字典)慢得多的主要原因:
import numpy as np
arr = np.arange(100000)
lst = list(range(100000))
def iterateover(obj):
for item in obj:
pass
%timeit iterateover(arr)
# 20.2 ms ± 155 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit iterateover(lst)
# 3.96 ms ± 26.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
set“构造函数”只是对对象进行迭代。
我无法肯定回答的一件事是为什么tolist 方法要快得多。最后,生成的 Python 列表中的每个值都需要位于“Python 盒子”中,因此tolist 可以 避免的工作并不多。但我确定的一件事是list(array) 比array.tolist() 慢:
arr = np.arange(100000)
%timeit list(arr)
# 20 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit arr.tolist()
# 10.3 ms ± 253 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
每个都具有O(n) 运行时复杂性,但常数因素非常不同。
在您的情况下,您确实比较了 set() 和 tolist() - 这不是一个特别好的比较。将set(arr) 与list(arr) 或set(arr.tolist()) 与arr.tolist() 进行比较会更有意义:
arr = np.random.randint(0, 1000, (10000, 3))
def tosets(arr):
for line in arr:
set(line)
def tolists(arr):
for line in arr:
list(line)
def tolists_method(arr):
for line in arr:
line.tolist()
def tosets_intermediatelist(arr):
for line in arr:
set(line.tolist())
%timeit tosets(arr)
# 72.2 ms ± 2.68 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists(arr)
# 80.5 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit tolists_method(arr)
# 16.3 ms ± 140 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit tosets_intermediatelist(arr)
# 38.5 ms ± 200 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
所以如果你想要sets,你最好使用set(arr.tolist())。对于更大的数组,可能使用np.unique 是有意义的,但因为您的行只包含 3 个可能会更慢的项目(对于数千个元素,它可能要快得多! )。
在您询问有关 numba 的 cmets 中,是的,numba 确实可以加快这一速度。 Numba supports typed sets (only numeric types),但这并不意味着它会总是更快。
我不确定 numba(重新)如何实现 sets,但由于它们是键入的,因此它们很可能也避免了“Python 框”并将值直接存储在 set 中:
集合比lists 更复杂,因为它涉及hashes 和空槽(Python 对集合使用开放寻址,所以我认为 numba 也会如此)。
与 NumPy array 一样,numba set 直接保存值。因此,当您将 NumPy array 转换为 numba set(或反之亦然)时,根本不需要使用“Python 盒子”,因此当您在 numba 中创建 sets 时nopython 函数它会比set(arr.tolist()) 操作快得多:
import numba as nb
@nb.njit
def tosets_numba(arr):
for lineno in range(arr.shape[0]):
set(arr[lineno])
tosets_numba(arr) # warmup
%timeit tosets_numba(arr)
# 6.55 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
这比set(arr.tolist()) 方法快大约五倍。但重要的是要强调我没有从函数中返回sets。当您从 nopython numba 函数返回 set 到 Python Numba 时,会创建一个 python 集 - 包括为集中的所有值“创建框”(这是 numba 隐藏的东西)。
仅供参考:如果您将 lists 传递给 Numba nopython 函数或从这些函数返回列表,则会发生相同的装箱/拆箱。那么 Python 中的 O(1) 操作是 Numba 中的 O(n) 操作!这就是为什么通常最好将 NumPy 数组传递给 numba nopython 函数(即O(1))。
我假设如果您从函数中返回这些集合(现在实际上不可能,因为 numba 目前不支持集合列表)它会更慢(因为它创建了一个 numba 集合 并且 稍后将其转换为 python 集)或仅稍微快一点(如果转换 numbaset -> pythonset 真的非常快)。
仅当我不需要从函数中返回它们并且在函数内对集合执行所有操作并且时,我个人才会将 numba 用于集合在 nopython 模式下支持。在任何其他情况下,我都不会在这里使用 numba。
请注意:from numpy import * 应该避免使用,这样做时会隐藏几个 python 内置函数(sum,min,max,...),它会放很多东西进入你的全局变量。最好使用import numpy as np。函数调用前面的np. 使代码更清晰,无需输入太多内容。