列表在python中表现不佳?

时间:2011-07-04 11:34:55

标签: python

我试图从一些巨大的文件中读取数据并将其写回来,但我意识到主要的成本来自于将数据分配到列表而不是从/向文件读取或写入数据....

    rows = [None] * 1446311
    begin = datetime.datetime.now()
    for i in range( 1446311 ):
       row = csvReader.next()
       rows[i] = row
    print datetime.datetime.now() - begin

上面的代码需要18秒但是5秒,如果我注释掉第5行(rows[i] = row),我已经预先建立了列表(即保留了内存),但为什么它仍然如此慢?我能做什么让它更快?我试过row for row in csvReader,但表现更差......

的问候, 约翰

2 个答案:

答案 0 :(得分:6)

我得到了类似的结果,但不像你的那么戏剧化。 (注意使用timeit模块来执行时间代码,并注意我已经考虑了列表创建,因为它对两个测试用例都是通用的。)

import csv
from timeit import Timer

def write_csv(f, n):
    """Write n records to the file named f."""
    w = csv.writer(open(f, 'wb'))
    for i in xrange(n):
        w.writerow((i, "squared", "equals", i**2))

def test1(rows, f, n):
    for i, r in enumerate(csv.reader(open(f))):
        rows[i] = r

def test2(rows, f, n):
    for i, r in enumerate(csv.reader(open(f))):
        pass

def test(t): 
    return (Timer('test%d(rows, F, N)' % t,
                  'from __main__ import test%d, F, N; rows = [None] * N' % t)
            .timeit(number=1))

>>> N = 1446311
>>> F = "test.csv"
>>> write_csv(F, N)
>>> test(1)
2.2321770191192627
>>> test(2)
1.7048690319061279

这是我对发生了什么的猜测。在两个测试中,CSV读取器从文件中读取记录,并在内存中创建表示该记录的数据结构。

在未存储记录的test2中,数据结构会或多或少立即被删除(在循环的下一次迭代中,row变量被更新,因此引用计数为先前的记录递减,因此回收内存)。这使得用于前一条记录的内存可以重用:这个内存已经在计算机的虚拟内存表中,并且可能仍然在缓存中,所以它(相对)快。

在存储记录的test1中,每条记录必须在新的内存区域中分配,该区域必须由操作系统分配,并复制到缓存中,因此它(相对)慢。

因此列表分配不会占用时间,而是内存分配


这是另外几个测试,用于说明正在发生的事情,没有csv模块的复杂因素。在test3中,我们为每一行创建一个新的100元素列表,并存储它。在test4中,我们为每一行创建一个新的100元素列表,但我们不存储它,我们将它丢弃,以便下次循环时可以重用内存。

def test3(rows, f, n):
    for i in xrange(n):
        rows[i] = [i] * 100

def test4(rows, f, n):
    for i in xrange(n):
        temp = [i] * 100
        rows[i] = None

>>> test(3)
9.2103338241577148
>>> test(4)
1.5666921138763428

所以我认为,如果您不需要同时将所有行存储在内存中,请不要这样做!如果可以的话,一次一个地读取它们,一次处理一个,然后忘记它们,以便Python可以解除分配它们。

答案 1 :(得分:0)

编辑:第一部分不是那么有效(见下面的评论)

你有没有试过这样的话:

rows = [None] * 1446311
for i in range( 1446311 ):
   rows[i] = csvReader.next()

因为根据我在代码中看到的内容,您将数据复制两次:一次从文件到内存row = ...,一次从rowrows[i]。由于你在这里有不可变的东西(字符串),我们真的在谈论数据的复制,而不是关于引用的复制。

此外,即使您之前创建了一个空列表,也会将大量数据放入其中;因为你只将None放在开头,所以没有保留真正的内存空间。所以也许你也可以直接写一个非常简单的事情:

rows = []
for i in range( 1446311 ):
   rows.append(csvReader.next())

或者甚至可以直接使用生成器语法!

rows = list(csvReader)

EDIT 在阅读Gareth的回答后,我对我的提案做了一些时间测试。顺便说一句,在从迭代器读取时要注意保护,以便在迭代器比预期更短时停止:

>>> from timeit import Timer
>>> import csv
>>> # building some timing framework:
>>> def test(n):
    return min(Timer('test%d(F, N)' % t,
                  'from __main__ import test%d, F, N' % t)
            .repeat(repeat=10, number=1))

>>> F = r"some\big\csvfile.csv"
>>> N = 200000
>>> def test1(file_in, number_of_lines):
    csvReader = csv.reader(open(file_in, 'rb'))
    rows = [None] * number_of_lines
    for i, c in enumerate(csvReader):  # using iterator syntax
        if i > number_of_lines:  # and limiting the number of lines
            break
        row = c
        rows[i] = row
    return rows

>>> test(1)
0.31833305864660133

>>> def test2(file_in, number_of_lines):
    csvReader = csv.reader(open(file_in, 'rb'))
    rows = [None] * number_of_lines
    for i, c in enumerate(csvReader):
        if i > number_of_lines:
            break
        row = c
    return rows

>>> test(2)
0.25134269758603978  # remember that only last line is stored!

>>> def test3(file_in, number_of_lines):
    csvReader = csv.reader(open(file_in, 'rb'))
    rows = [None] * number_of_lines
    for i, c in enumerate(csvReader):
        if i > number_of_lines:
            break
        rows[i] = c
    return rows

>>> test(3)
0.30860502255637812

>>> def test4(file_in, number_of_lines):
    csvReader = csv.reader(open(file_in, 'rb'))
    rows = []
    for i, c in enumerate(csvReader):
        if i > number_of_lines:
            break
        rows.append(c)
    return rows

>>> test(4)
0.32001576256431008

>>> def test5(file_in, number_of_lines):
    csvReader = csv.reader(open(file_in, 'rb'))
    rows = list(csvReader)  
    # problem: there's no way to limit the number of lines to parse!
    return rows

>>> test(5)
0.30347613834584308

我们可以看到,对于大于文档中行数的N,时间上没有太大差异。 test2,在我的机器上,意外地只是有点不同。 test5更优雅,但不能限制解析的行数,这可能很烦人。

所以,如果你一次需要所有行,我的建议就是采用最优雅的解决方案,即使有点长:test4。但也许,正如Gareth所说的那样,你不需要同时拥有一切,这是获得速度和记忆的最佳方式。