【问题标题】:Fastest way to chain multiple indexing operations for lists?为列表链接多个索引操作的最快方法?
【发布时间】:2017-06-16 03:17:51
【问题描述】:

我想对我拥有的一些数据进行分类,为此我想对 python 列表进行链式索引。简化我有一个嵌套列表:

lst = [[[1], [2]], [[3, 3], [4]], [[5], [6,6,6]]]

我想迭代前两个索引的乘积,但保持第三个相同:

from itertools import product

for index1, index2 in product(range(3), range(2)):
    print(lst[index1][index2][0])

但是,我想在不事先知道需要深入多少子结构的情况下使其更通用(我想让ranges 的数量传递给itertools.product 变量)。

我有点挣扎如何概括[index1][index2][0] 以接受indices 的任意数字,我能想到的最好的数字是functools.reduce

from functools import reduce

for indices in product(range(3), range(2)):
    print(reduce(list.__getitem__, indices, lst)[0])

这看起来非常复杂(并且比手动索引慢得多),所以我想知道是否有更好更快的方法来做到这一点。我正在使用 python 2.x 和 3.x,外部库绝对没问题(但它不应该需要 NumPy 或基于 NumPy 的包)。

【问题讨论】:

  • 根据你的说法“但是我想让这个更一般化,而不事先知道这需要深入多少子结构。” 你的意思是列表的深度是未知的即它可以是 n 级深/嵌套?
  • 对不起,如果我不清楚,变量部分是 rangesnumber (n) 给 product (以及 indices 的长度) )。列表的深度是“未知的”,但至少有 n+1 子结构深。我会更新问题。

标签: python performance list iteration


【解决方案1】:

我提出一种递归方式。

def theshape(lst):
    l=lst
    shape=[]
    while isinstance(l,list):
                shape.append(len(l))
                l=l[0]
    return shape

此函数旨在查找结构的形状,该形状在最后一维之前应该是规则的。

def browse(lst):
    shape=theshape(lst)
    ndim=len(shape)
    def level(l,k):
        if k==ndim:
            print(l)
        else:
            for i in range(shape[k]):
                level(l[i],k+1)
    level(lst,0)

这个递归浏览所有级别。它最大限度地减少指针变化。

一个简单的例子:

u=arange(2**6).reshape(4,2,1,2,2,1,1,2).tolist()
browse(u)
0
2
.
.
.
62

对大型结构的一些测试(print = lambda _ : None 丢弃了打印):

def go(lst):
 for x in product(*[range(k) for k in theshape(lst)]):
    print(reduce(lambda result, index: result[index], x, lst))

In [1]: u=arange(2**21).reshape([2]*21).tolist()

In [2]: %time go(u)
Wall time: 14.8 s

In [3]: %time browse(u)
Wall time: 3.5 s

In [5]: u=arange(2**21).reshape([1]*30+[2**21]+[1]).tolist()

In [6]: %time go(u)
Wall time: 18 s

In [7]: %time browse(u)
Wall time: 3.48 s

In [8]: u=arange(2**21).reshape([1]+[2**21]+[1]*30).tolist()

In [9]: %time go(u)
Wall time: 14 s

In [10]: %time browse(u)
Wall time: 58.1 s

这表明性能非常依赖于数据结构。

编辑:

最后,最简单的就是最快的。 theshape 不是必需的。

def browse2(lst):
        if isinstance(lst,list):
            for l in lst:
                browse2(l)
        else: print(lst)

它通常比浏览快 30%。无论列表的结构如何,它都可以工作。

【讨论】:

  • 哇,这令人困惑,我喜欢这种方法,但像 shape=shape(lst) 这样的行看起来很奇怪 :-) 我稍后会检查该方法是否像我预期的那样工作。
  • 我添加了一个新的最简单的版本。
  • 其实我之前错过了,但是这会访问嵌套最深的[0] 元素,我实际上想以固定的嵌套和预先确定的“范围”访问[0] 元素。输入并不总是“规则形状”,因此递归方法有其缺点。我希望你不介意我接受了直接解决我问题的(更快的)其他答案。
【解决方案2】:

我会为此使用 python-builtin reduce,它看起来并没有那么复杂,而且在我的测试中也没有那么慢:

from itertools import product

for x in product(range(3), range(2)):
    rg = reduce(lambda result, index: result[index], x, lst)
    value = rg[0]

如果您担心来自reduce 的时间损失,您可以改用for 循环:

for x in product(range(3), range(2)):
    value = lst
    for index in x:
        value = value[index]
    value = value[0]

在所有情况下,这都会比手动索引慢,因为for 循环需要额外的操作来确定停止条件。与往常一样,问题是对于任意深度规范的灵活性而言,速度优化对您是否值得。

至于为什么要使用reducefor,JavaScript 社区中一直存在关于是否应该在@987654333 上使用reducemapfilter 函数的风格争论@s 或改用 for 循环版本,因为它更快,您可能需要参考该辩论来选择您支持哪一方。


用 for 循环计时:

In [22]: stmt = '''
    ...: from itertools import product
    ...: def go():
    ...:   lst = [[[1], [2]], [[3, 3], [4]], [[5], [6,6,6]]]
    ...:   for x in product(range(3), range(2)):
    ...:     # rg = reduce(lambda result, index: result[index], x, lst)
    ...:     value = lst
    ...:     for index in x:
    ...:         value = value[index]
    ...:     value = value[0]
    ...:     # value = lst[x[0]][x[1]][0]
    ...: '''

In [23]: timeit(setup=stmt, stmt='go()', number=1000000)
Out[23]: 4.003296852111816

定时reduce:

In [18]: stmt = '''
    ...: from itertools import product
    ...: def go():
    ...:   lst = [[[1], [2]], [[3, 3], [4]], [[5], [6,6,6]]]
    ...:   for x in product(range(3), range(2)):
    ...:     rg = reduce(lambda result, index: result[index], x, lst)
    ...:     value = rg[0]
    ...:     # value = lst[x[0]][x[1]][0]
    ...: '''

In [19]: timeit(setup=stmt, stmt='go()', number=1000000)
Out[19]: 6.164631128311157

手动索引时间:

In [16]: stmt = '''
    ...: from itertools import product
    ...: def go():
    ...:   lst = [[[1], [2]], [[3, 3], [4]], [[5], [6,6,6]]]
    ...:   for x in product(range(3), range(2)):
    ...:     # rg = reduce(lambda result, index: result[index], x, lst)
    ...:     value = lst[x[0]][x[1]][0]
    ...: '''

In [17]: timeit(setup=stmt, stmt='go()', number=1000000)
Out[17]: 3.633723020553589

【讨论】:

  • for-loop 似乎是最快、最直接的方法。谢谢!
【解决方案3】:

如何动态创建硬索引?

lst = [[[1], [2]], [[3, 3], [4]], [[5], [6,6,6]]]

from itertools import product

for index1, index2 in product(range(3), range(2)):
    print(lst[index1][index2][0])


# need depth info from somewhere to create hard coded indexing

prod_gen = product(range(3), range(2))

first = next(prod_gen)

indx_depth = len(first) + 1

exec( ('def IndexThisList(lst, indxl):\n' +
       '        return lst' + ''.join(('[indxl[' + str(i) + ']]' 
                                           for i in range(indx_depth)))))

# just to see what it exec'd:
print(("def IndexThisList(lst, indx_itrbl):\n" +
       "        return lst" + ''.join(('[indx_itrbl[' + str(i) + ']]' 
                                       for i in range(indx_depth)))))
# the exec is only invoked again when changing the indexing depth
# for accessing the list with its currently instantiated depth of indexing
# just use the current instance of the generated function

print(IndexThisList(lst, first + (0,)))
for itpl in prod_gen: 
    print (IndexThisList(lst, itpl + (0,)))

1
2
3
4
5
6
def IndexThisList(lst, indx_itrbl):
        return lst[indx_itrbl[0]][indx_itrbl[1]][indx_itrbl[2]]
1
2
3
4
5
6

这只是一个初学者,似乎我的 exec 应该用另一个函数包装以传递 index_depth 但它现在躲避我

【讨论】:

  • 为什么?当您自己的代码是唯一提供代码字符串的东西时,没有安全问题
  • 主要是因为正确设置它们很麻烦 - 更糟糕的是,它们真的很难维护。
  • “最快”并不总是最优雅的,确实需要与时间赛跑
  • 我已经计时了,exec 似乎是一个瓶颈,执行exec 大约需要 100us,这已经超过了其他函数用于中等大小的输入。该函数也比for-loop(独立于exec-timings)慢,可能是因为函数调用开销。
猜你喜欢
  • 1970-01-01
  • 2022-06-13
  • 2019-09-02
  • 2014-10-18
  • 2021-06-15
  • 2016-06-09
  • 2019-11-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多