【问题标题】:How to speed up nested for loops in Python如何加快 Python 中的嵌套 for 循环
【发布时间】:2016-11-10 20:28:52
【问题描述】:

我有以下 Python 2.7 代码:

listOfLists = []
for l1_index, l1 in enumerate(L1):
    list = []
    for l2 in L2:
        for l3_index,l3 in enumerate(L3):
            if (L4[l2-1] == l3):
                value = L5[l2-1] * l1[l3_index]
                list.append(value)
                break
    listOfLists.append(list)

L1,L2,L3,L4,L5 列表为:

L1 = [[0.60, 0.95, 0.38, 1.02, 0.29, 0.43], [0.40, 0.09, 0.87, 0.85, 0.70, 0.46], [0.67, 0.91, 0.66, 0.79, 0.86, 0.06], [0.59, 1.81, 0.05, 1.88, 0.20, 0.48], [0.64, 0.34, 0.37, 1.39, 0.56, 0.27], [0.56, 0.34, 0.68, 2.79, 0.18, 0.42], [0.42, 1.67, 0.04, 0.44, 0.25, 0.94], [0.32, 1.92, 0.95, 2.85, 0.95, 0.96], [0.50, 0.68, 0.84, 1.79, 0.35, 0.09], [0.34, 0.66, 0.85, 0.35, 0.38, 0.59], [0.50, 0.79, 0.45, 2.93, 0.50, 0.92], [0.11, 0.11, 0.93, 1.11, 0.81, 0.49]]  # a list of 12 sublists
L2 = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
L3 = [480, 120, 35, 0, 520, 300]
L4 = [120, 120, 120, 0, 300, 35, 35, 520, 300, 480, 120, 480, 0, 35, 0, 0, 300]
L5 = [5.4, 2.83, 1.16, 6.9, 0.76, 2.15, 5.61, 3.12, 1.57, 0.08, 5.36, 0.2, 1.2, 1.4, 2.9, 2.1, 3.5]

这些只是示例;实际上,这些列表包含数十万个数字。解释器需要几十秒来计算三个嵌套的for 循环。

是否有可能以某种方式加速此代码,例如使用itertools 或任何其他模块/功能?

编辑:我不能使用非标准的 python 2.7 模块(numpy、scipy...)

【问题讨论】:

  • 是更大数据结构的列表部分,那么 numpy 应该能够完成这项工作
  • @ChristianSauer 谢谢你的回复,我很抱歉没有提到我不能使用任何需要额外安装的python 2.7模块,比如numpy。
  • 你能提供每个向量的长度吗?如果它们的长度相同,您可以使用zip。您也可以使用列表推导,因为它太难读了。
  • 您可以用 C/C++ 编写代码并将其导入 Python (docs.python.org/2/extending/extending.html) 吗?
  • 由于我们不知道您列表中的数据意味着什么以及您尝试执行什么样的操作,因此甚至很难概念化一个答案。底线不是如何加速嵌套循环,是我什至需要嵌套循环来实现我想要的。换个说法-您需要找到更好的算法来执行您想要的操作(或者重新考虑使用的数据结构)。嵌套循环总是很麻烦,因为如果列表大小大致相同 n 并且存在 m 嵌套循环,它实际上会产生类似 O(n^m) 的问题。切换到编译语言不会改变复杂性。

标签: python for-loop nested


【解决方案1】:

以下代码是@spacegoing 和@Alissa 的组合,产生最快的结果:

L3_dict = {l3:l3_index for l3_index,l3 in enumerate(L3)}

list_of_lists = [[L5[l2 - 1] * l1[L3_dict[L4[l2-1]]]
     for l2 in L2]
 for l1 in L1]

感谢@spacegoing 和@Alissa 的耐心和时间。

【讨论】:

    【解决方案2】:

    @Rogalski 是对的,您肯定需要重新考虑算法(至少尝试这样做)。

    但是如果你找不到更好的算法,我认为你可以通过一些技巧来加快速度,同时仍然使用嵌套循环。请注意,我将 L* 列表视为一些全局变量,我不需要将其传递给每个函数。因此,您需要让这些列表对新函数可见,或者将它们作为参数添加。

    首先,尝试清理。例如,您似乎从不使用 l1_index,因此您可以摆脱它。然后,您可以将第一个循环内发生的所有事情移动到一个函数中。然后它看起来像这样:

    listOfLists = []
    for l1 in L1:
        listOfLists.append(create_list(l1))
    
    def create_list(l1):
        list = []
        for l2 in L2:
            for l3_index,l3 in enumerate(L3):
                if (L4[l2-1] == l3):
                    value = L5[l2-1] * l1[l3_index]
                    list.append(value)
                    break
        return list
    

    这很好,但是推导比使用追加循环更快(here 你可以找到一篇关于该主题的好文章)。第一个循环非常简单,所以我们把它折叠成listOfLists = [create_list(l1) for l1 in L1]。我们可以在 create_list 函数上执行相同的内部循环提取

    list_of_lists = [create_list(l) for l in L1]
    
    def create_list(l):
        return [find_next(l, element) for element in L2]
    
    def find_next(l, element):
        for l3_index, l3_element in enumerate(L3):
            if (L4[element - 1] == l3_element):
                return L5[element - 1] * l[l3_index] 
    

    现在它看起来更具可读性,并且应该运行得更快一些。您也可以尝试使用内置列表功能来查找列表中的元素(l3_index = l3.index(L4[element-1]), ),但我不知道它是否会更快。

    请注意,lambda 并不比以相同方式执行相同操作的普通函数快。但是它们确实破坏了堆栈跟踪,从而使代码更难调试。从 itertools 开始,您可以使用组合,但是您需要预先生成 list_of_lists,因为没有按顺序为您提供组合的合同。而且 zip 并不是您所需要的。

    代码的一个问题是您在嵌套循环的每一轮中循环通过 L3。解决这个问题的方法是添加一些预先计算。您需要知道 L4 的每个元素对应的 L3 索引。你可以这样做:

    # this will allow you to get index by element at a constant time
    # and it only takes O(N)
    L3_dict = {element:index for element,index in enumerate(L3)}
    
    list_of_lists = [create_list(l) for l in L1]
    
    def create_list(l):
        return [find_next(l, element) for element in L2]
    
    def find_next(l, element):
        # if you use dict, you reduce time of this method from O(N) to constant
        # as both access to dict item by key and to list item by index
        # are done in a constant time
        l3_index = L3_dict[L4[element-1]]
        return L5[element-1] * l[l3_index]
    

    【讨论】:

    • 斯帕西博艾丽莎!我确实使用了l1_index,但我没有发布使用它的代码。尽管如此,我可以在没有l1_index 变量的情况下生活。信不信由你,您发布的上层代码实际上比我最初回复中的代码需要更多的执行时间。它慢了 0.5 秒。使用列表推导式是否存在一些速度缺陷?
    • 这很奇怪,通常[smth(i) for i in someList] 之类的结构比for i in someList: newList.append(smth(i)) 之类的结构要快如果将第一行替换为list_of_lists = [[find_next(l,element) for element in L2] for l in L1] 会发生什么?
    • 顺便问一下,您可以控制输入吗?如果将其中一些转换为字典,则可以节省大量时间...您说有系数,通常可以将这些系数存储在字典中
    • 嗨@Alissa。谢谢你的另一个建议。可悲的是list_of_lists = [[find_next(l,element) for element in L2] for l in L1] 导致与非线程版本相同的运行时间。我也不确定为什么理解列表会产生相同的运行时间。我确实可以控制输入。您是否建议将 l1 元素放在 L1_dictionary 中?
    • 不,我的意思是您可以识别通过简单规则匹配的列表对并将它们设为字典。例如。 l3_index 是与 L4 中某个元素匹配的元素的索引。你能制作一个包含 L4 元素作为键和 l3 索引作为值的 dict(然后你不会遍历 L3)
    【解决方案3】:

    既然你说the readability is not important as long as it speeds up the code,这就是你的伎俩:

    [[L5[l2 - 1] * sl1 for sl1, l3 in zip(l1, L3)
         for l2 in L2 if L4[l2 - 1] == l3]
     for l1 in L1]
    

    此代码比 for 循环快 25%。但相信我,无论是谁在我的代码中写了这个,我都会开枪打死他。

    【讨论】:

    • 谢谢@spacegoing!事实上,代码现在更快了!它真的需要像你那样放在三行吗?如果只有一行会不会更快?
    • @marco 欢迎您。该格式样式仅用于您的可读性。无论您如何格式化它们,速度都是一样的。
    • 明白。再一次感谢你。是否可以张贴您的姓名,以便我在源代码中注明您的姓名?
    • @marco 非常感谢您的好意。我宁愿你不要在你的代码中提到我,这样人们就不会恨我了哈哈。
    • 明白。谢谢。
    猜你喜欢
    • 2020-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    • 1970-01-01
    • 2019-04-15
    • 2018-06-01
    • 2019-06-27
    相关资源
    最近更新 更多