【问题标题】:Fastest way to construct tuple from elements of list (Python)从列表元素构造元组的最快方法(Python)
【发布时间】:2018-12-05 13:45:16
【问题描述】:

我有 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 数组。我将它们称为列表并显示它们是我的错误。它们的长度相同。

【问题讨论】:

  • list1,2,3的长度总是一样的吗?
  • 你有列表或 NumPy 数组吗?另外,如果您要创建元组,为什么要创建字典?另外,mydict[(1,6,11)] 到底应该是什么?
  • 无论如何,如果这些 NumPy数组,只需将它们堆叠成一个二维数组,然后转置数组。这需要固定的时间——它只是以不同的步幅对相同的数据创建一个新视图。
  • 你能发布一个工作示例吗?虚拟化一些小数组应该很容易。
  • 为什么你认为这不必要地慢?您一次只能为一个字典索引一个键。 zip() 是“转置”列表列表的标准方式。

标签: python list numpy tuples


【解决方案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)

【讨论】:

    【解决方案2】:

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

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

    如果是这样,您可以将它们堆叠在一个二维数组中:

    >>> 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)]
    

    如果您担心在 dict 中查找键元组的时间,itemgetter 模块中的 operator 具有 C 加速版本。如果keysnp.array,或者tuple,或者其他什么,你可以这样做:

    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/键。)

    【讨论】:

    • @miradulo 是的,我假设 ll 将是 OP 真正想要的东西,而 lt 将只是用于一些一次性过程并被扔掉......但这是一个完全基于几乎没有的猜测,所以我编辑了答案。
    猜你喜欢
    • 1970-01-01
    • 2011-06-01
    • 2019-09-06
    • 2011-05-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-03
    • 1970-01-01
    相关资源
    最近更新 更多