从列表元素构造元组的最快方法(Python)

时间:2018-06-26 18:48:27

标签: python list numpy tuples

我有3个NumPy数组,我想为每个列表的第i个元素创建元组。这些元组代表我先前定义的字典的键。

例如:

List 1: [1, 2, 3, 4, 5]

List 2: [6, 7, 8, 9, 10]

List 3: [11, 12, 13, 14, 15]

Desired output: [mydict[(1,6,11)],mydict[(2,7,12)],mydict[(3,8,13)],mydict[(4,9,14)],mydict[(5,10,15)]]

这些元组代表我先前定义的字典的键(本质上是作为先前计算函数的输入变量)。我读过,这是存储用于查找的函数值的最佳方法。

我当前执行此操作的方法如下:

[dict[x] for x in zip(l1, l2, l3)]

这有效,但是显然很慢。是否可以向量化此操作,或以任何方式使其更快?如果需要的话,我也愿意改变存储函数值的方式。

编辑:对于这个问题,我深表歉意。我确实有NumPy数组。将它们称为列表并如此显示它们的错误。它们的长度相同。

2 个答案:

答案 0 :(得分:3)

您的问题有点令人困惑,因为您正在调用这些NumPy数组,并要求一种矢量化事物的方法,然后显示列表,并将其标记为示例中的列表,并在标题中使用list。我将假设您确实有数组。

>>> l1 = np.array([1, 2, 3, 4, 5])
>>> l2 = np.array([6, 7, 8, 9, 10])
>>> l3 = np.array([11, 12, 13, 14, 15])

如果是这样,您可以将它们堆叠成2D阵列:

>>> ll = np.stack((l1, l2, l3))

然后您可以将其转置:

>>> lt = ll.T

这比矢量化要好;这是恒定时间。 NumPy只是创建相同数据的另一个视图,但步幅不同,因此它以列顺序而不是行顺序读取。

>>> lt
array([[ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14],
       [ 5, 10, 15]])

正如miradulo所指出的,您可以使用column_stack一步完成这两个步骤:

>>> lt = np.column_stack((l1, l2, l3))

但是我怀疑您实际上会想要ll作为其本身的价值。 (尽管我承认我只是在猜测您要做什么...)


当然,如果您想将这些行作为一维数组循环而不是进行进一步的矢量化工作,则可以:

>>> for row in lt:
...:     print(row)
[ 1  6 11]
[ 2  7 12]
[ 3  8 13]
[ 4  9 14]
[ 5 10 15]

当然,您只需在每一行调用tuple,就可以将它们从一维数组转换为元组。或者……不管mydict是什么(看起来不像字典,没有键值对,只有值),您可以做到这一点。

>>> mydict = collections.namedtuple('mydict', list('abc'))
>>> tups = [mydict(*row) for row in lt]
>>> tups
[mydict(a=1, b=6, c=11),
 mydict(a=2, b=7, c=12),
 mydict(a=3, b=8, c=13),
 mydict(a=4, b=9, c=14),
 mydict(a=5, b=10, c=15)]

如果您担心在字典中查找键元组的时间,则operator模块中的itemgetter具有C加速版本。如果keysnp.arraytuple或其他任何方法,则可以执行以下操作:

for row in lt:
    myvals = operator.itemgetter(*row)(mydict)
    # do stuff with myvals

与此同时,我决定将C扩展尽可能快地拍打起来(没有错误处理,因为我很懒它应该以这种方式更快一点—这段代码如果您给它除字典和元组或列表以外的任何内容,都可能会出现段错误:

static PyObject *
itemget_itemget(PyObject *self, PyObject *args) {
  PyObject *d;
  PyObject *keys;
  PyArg_ParseTuple(args, "OO", &d, &keys);    
  PyObject *seq = PySequence_Fast(keys, "keys must be an iterable");
  PyObject **arr = PySequence_Fast_ITEMS(seq);
  int seqlen = PySequence_Fast_GET_SIZE(seq);
  PyObject *result = PyTuple_New(seqlen);
  PyObject **resarr = PySequence_Fast_ITEMS(result);
  for (int i=0; i!=seqlen; ++i) {
    resarr[i] = PyDict_GetItem(d, arr[i]);
    Py_INCREF(resarr[i]);    
  }
  return result;
}

在macOS上使用python.org CPython 3.7在笔记本电脑上从10000键字典中查找100个随机键的时间:

  • itemget.itemget:1.6µs
  • operator.itemgetter:1.8µs
  • 理解力:3.4µs
  • 纯Python operator.itemgetter:6.7µs

因此,我非常确定您所做的任何事情都将足够快-我们正在尝试优化的键值仅为34ns / key。但是,如果这确实太慢,则operator.itemgetter可以很好地完成将循环移动到C并将其削减大约一半的工作,这非常接近您可以预期的最佳结果。 (毕竟,很难想象以不到16ns / key的价格在哈希表中循环一堆盒装值的键。)

答案 1 :(得分:1)

定义您的3个列表。您提到了3个数组,但显示了列表(也称其为列表):

In [112]: list1,list2,list3 = list(range(1,6)),list(range(6,11)),list(range(11,16))

现在使用元组键创建字典:

In [114]: dd = {x:i for i,x in enumerate(zip(list1,list2,list3))}
In [115]: dd
Out[115]: {(1, 6, 11): 0, (2, 7, 12): 1, (3, 8, 13): 2, (4, 9, 14): 3, (5, 10, 15): 4}

使用您的代码访问该词典中的元素:

In [116]: [dd[x] for x in zip(list1,list2,list3)]
Out[116]: [0, 1, 2, 3, 4]
In [117]: timeit [dd[x] for x in zip(list1,list2,list3)]
1.62 µs ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

现在使用等效数组-将列表转换为二维数组:

In [118]: arr = np.array((list1,list2,list3))
In [119]: arr
Out[119]: 
array([[ 1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10],
       [11, 12, 13, 14, 15]])

访问相同的字典元素。如果我使用column_stack,本可以省略.T,但这要慢一些。 (数组转置速度很快)

In [120]: [dd[tuple(x)] for x in arr.T]
Out[120]: [0, 1, 2, 3, 4]
In [121]: timeit [dd[tuple(x)] for x in arr.T]
15.7 µs ± 21.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

请注意,这实际上要慢一些。数组上的迭代比列表上的迭代慢。您无法以任何numpy的“矢量化”方式访问字典中的元素-您必须使用Python迭代。

我可以通过首先将其转换为列表来改进数组迭代:

In [124]: arr.T.tolist()
Out[124]: [[1, 6, 11], [2, 7, 12], [3, 8, 13], [4, 9, 14], [5, 10, 15]]
In [125]: timeit [dd[tuple(x)] for x in arr.T.tolist()]
3.21 µs ± 9.67 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

数组构建时间:

In [122]: timeit arr = np.array((list1,list2,list3))
3.54 µs ± 15.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [123]: timeit arr = np.column_stack((list1,list2,list3))
18.5 µs ± 11.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

使用纯Python itemgetter(来自v3.6.3)不会节省任何费用:

In [149]: timeit operator.itemgetter(*[tuple(x) for x in arr.T.tolist()])(dd)
3.51 µs ± 16.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

并且如果我将getter定义移出时间循环:

In [151]: %%timeit idx = operator.itemgetter(*[tuple(x) for x in arr.T.tolist()]
     ...: )
     ...: idx(dd)
     ...: 
482 ns ± 1.85 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)