【问题标题】:Python append performancePython追加性能
【发布时间】:2011-08-05 12:19:54
【问题描述】:

我在 Python 中使用“附加”时遇到了一些性能问题。 我正在编写一个算法来检查一组(大)圆中是否有两个重叠的圆。 我首先将圆的极值点 (x_i-R_i & x_i+R_i) 放在一个列表中,然后对列表进行排序。

class Circle:
def __init__(self, middle, radius):
    self.m = middle
    self.r = radius

在这之间,我生成了 N 个随机圆圈并将它们放入“圆圈”列表中。

"""
Makes a list with all the extreme points of the circles.
Format = [Extreme, left/right ~ 0/1 extreme, index]
Seperate function for performance reason, python handles local variables faster.
Garbage collect is temporarily disabled since a bug in Python makes list.append run in O(n) time instead of O(1)
"""
def makeList():
    """gc.disable()"""
    list = []
    append = list.append
    for circle in circles:
        append([circle.m[0]-circle.r, 0, circles.index(circle)])
        append([circle.m[0] + circle.r, 1, circles.index(circle)])
    """gc.enable()"""
    return list

当运行 50k 圈时,生成列表需要 75 秒以上。正如你可能在我写的 cmets 中看到的那样,我禁用了垃圾收集,把它放在一个单独的函数中,使用

append = list.append
append(foo)

而不仅仅是

list.append(foo)

我禁用了 gc,因为经过一番搜索后,似乎 python 存在一个错误,导致 append 在 O(n) 而不是 O(c) 时间内运行。

那么这种方式是最快的方式还是有办法让它运行得更快? 非常感谢任何帮助。

【问题讨论】:

  • list 在 python 中不是一个好的变量名。
  • list 在任何语言中都不是一个好的变量名...
  • """String literals""" 不是# comments。并且文档字符串必须在函数内部,而不是在函数之前。
  • @eumiro,CrazyJugglerDrummer:是的,改为 cirkeList。 @Sven:还不习惯 Python 的评论方式,我会记住你的建议。

标签: python performance append


【解决方案1】:

尝试在 collections 包中使用deque 来追加大行数据,而不会降低性能。然后使用 List Comprehension 将双端队列转换回 DataFrame。

示例案例:

from collections import deque

d = deque()
for row in rows:
 d.append([value_x, value_y])

df = pd.DataFrame({'column_x':[item[0] for item in d],'column_y':[item[1] for item in d]})

这是一个真正的节省时间。

【讨论】:

    【解决方案2】:

    我刚刚尝试了几个测试来提高“附加”功能的速度。一定对你有帮助。

    1. 使用 Python
    2. 使用 list(map(lambda - 称为比 for+append 更快的方法
    3. 使用 Cython
    4. 使用 Numba - jit

    代码内容:获取 0 ~ 9999999 之间的数字,将它们平方,然后使用 append 将它们放入一个新列表中。

    1. 使用 Python

      import timeit
      
      st1 = timeit.default_timer()
      
      def f1():
      
          a = range(0, 10000000)
      
          result = []
          append = result.append
      
          for i in a:
              append( i**2 )
      
          return result
      
      f1()
      
      
      st2 = timeit.default_timer()
      print("RUN TIME : {0}".format(st2-st1))
      

    运行时间:3.7 秒

    1. 使用 list(map(lambda

      import timeit
      
      st1 = timeit.default_timer()
      
      result = list(map(lambda x : x**2 ,  range(0,10000000) ))
      
      st2 = timeit.default_timer()
      print("RUN TIME : {0}".format(st2-st1))
      

    运行时间:3.6 秒

    1. 使用 Cython

      • .pyx 文件中的编码

        定义 f1(): cpdef 双我 a = 范围(0, 10000000)

        result = []
        append = result.append
        
        for i in a:
            append( i**2 )
        
        return result
        

    我编译它并在 .py 文件中运行它。

    • 在 .py 文件中

      import timeit
      from c1 import *
      
      st1 = timeit.default_timer()
      
      f1()
      
      st2 = timeit.default_timer()
      print("RUN TIME : {0}".format(st2-st1))
      

    运行时间:1.6 秒

    1. 使用 Numba - jit

      import timeit
      from numba import jit
      
      st1 = timeit.default_timer()
      
      @jit(nopython=True, cache=True)
      def f1():
      
          a = range(0, 10000000)
      
          result = []
          append = result.append
      
          for i in a:
              append( i**2 )
      
          return result
      
      f1()
      
      st2 = timeit.default_timer()
      print("RUN TIME : {0}".format(st2-st1))
      

    运行时间:0.57 秒

    结论:

    正如你上面提到的,改变简单的追加形式提高了一点速度。并且使用 Cython 比使用 Python 快得多。然而,就“for+append”的速度提升而言,使用 Numba 是最好的选择!

    【讨论】:

      【解决方案3】:

      如果性能是一个问题,我会避免使用 append。相反,预先分配一个数组,然后将其填满。我也会避免使用索引来查找列表“圆圈”中的位置。这里是重写。它并不紧凑,但我敢打赌它会因为展开循环而很快。

      def makeList():
          """gc.disable()"""
          mylist = 6*len(circles)*[None]
          for i in range(len(circles)):
              j = 6*i
              mylist[j] = circles[i].m[0]-circles[i].r
              mylist[j+1] = 0
              mylist[j+2] = i
              mylist[j+3] = circles[i].m[0] + circles[i].r
              mylist[j+4] = 1
              mylist[j+5] = i
          return mylist
      

      【讨论】:

        【解决方案4】:

        代替

        for circle in circles:
            ... circles.index(circle) ...
        

        使用

        for i, circle in enumerate(circles):
            ... i ...
        

        这可能会将您的 O(n^2) 减少到 O(n)。

        你的整个makeList 可以写成:

        sum([[[circle.m[0]-circle.r, 0, i], [circle.m[0]+circle.r, 1, i]] for i, circle in enumerate(circles)], [])
        

        【讨论】:

        • @Harm 这是一个很好的建议——您还应该考虑为此使用双端队列(请参阅集合模块),因为据报道它在追加操作方面的性能略好。然后,您可能希望在最后将其转换为列表,因此节省的费用可能不会超过开销。
        • 啊,好吧,我一直在看错误的东西。谢谢你的回答:) 想解释一下为什么得到一个圆的索引是 O(N²)? O(n) 我会理解,因为在列表中获取对象的索引等于在列表中线性搜索直到找到该对象。
        • @Harm:在for循环的n次迭代中,每次都进行线性搜索,总共O(n*n)。
        • @Harm: 一个 .index() 是 O(n),但对列表中的每个元素都这样做是 O(n^2)。
        • 他滥用sum 来连接大量列表-sum([x, y, z] default)default + x + y + z。顺便说一句,它的性能相当欠佳 - 它首先在内存中创建一个大列表,然后与它们进行 n 次 O(n) 连接。更好的单行代码是list(item for i, circle in enumerate(circles) for item in ([circle.m[0]-circle.r, 0, i], [circle.m[0]+circle.r, 1, i]),它避免在构建列表之前将所有数据都保存在内存中,并在没有连接的情况下立即构建整个结果列表。
        【解决方案5】:

        您的性能问题不在于append() 方法,而在于您对circles.index() 的使用,这使得整个事情变得O(n^2)。

        进一步的(相对较小的)改进是使用列表解析而不是list.append()

        mylist = [[circle.m[0] - circle.r, 0, i]
                  for i, circle in enumerate(circles)]
        mylist += [[circle.m[0] + circle.r, 1, i]
                   for i, circle in enumerate(circles)]
        

        请注意,这会以不同的顺序提供数据(这应该无关紧要,因为您正计划对其进行排序)。

        【讨论】:

        • 我对 Python 还是很陌生,并且仍在学习 Python 风格的东西,比如列表推导。谢谢你的回答:)
        猜你喜欢
        • 1970-01-01
        • 2015-09-07
        • 2016-12-03
        • 2015-03-11
        • 1970-01-01
        • 1970-01-01
        • 2014-05-21
        • 1970-01-01
        • 2018-03-28
        相关资源
        最近更新 更多