Python中的List表现差吗?

4

我试图从一个超大文件中读取数据并将它们写回,但是我发现主要的成本来自于将数据分配给列表而不是从/写入文件中读取或写入数据....

    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行(rows[i] = row),只需要5秒。我已经提前构建了列表(即保留了内存),但为什么速度仍然很慢?有什么方法可以让它更快吗?我尝试过row for row in csvReader,但表现更差...

祝好, 约翰


我在运行时看不出L5存在与不存在之间有什么大的区别。(尽管我不得不伪造csvReader.next()调用,这可能会产生影响。) - user25148
正如Gareth所解释的那样,您没有为所有实际行预分配内存,而这种分配是耗费时间的原因。如果您不需要同时将所有行保存在内存中,则可以通过构建代码以使用生成器/生成器表达式来提高性能。 - John La Rooy
2个回答

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就可以对它们进行解除分配。


你提到的时间是从内存分配中获取的,你是否指的是列表内容的内存还是列表的引用(指针)?test3和test4仅为列表内容分配内存,即每个[i] * 100只分配一次。 - John
жҲ‘жҢҮзҡ„жҳҜжүҖжңүиЎҢзҡ„еҶ…еӯҳгҖӮжӯӨеӨ–пјҢжөӢиҜ•3е’Ң4еңЁеҫӘзҺҜдёӯжҜҸж¬ЎеҲҶй…ҚдёҖдёӘж–°зҡ„100е…ғзҙ еҲ—иЎЁгҖӮ - Gareth Rees
而且,你说下一个循环可以利用我们从上一个循环分配的内存,为什么这些内存会与其他“野”的但是空闲的内存块不同呢?因为我们已经解除引用了先前分配的内存,对吧? - John
释放的内存与“野生”(从未分配)的内存不同!“野生”内存需要由操作系统(页表等)分配,然后缓存;最近释放的内存则不需要。 - Gareth Rees
是的,我没想到那一点,即使下一个循环所需的内存大小可能比上一个循环大,Python 也可能会延迟取消引用内存的“缓存”。正如你所提到的,Python 中的内存分配不便宜。作为一个新的 Python 程序员,我认为我应该阅读一些关于 Python 内存管理的资料,因为 Python 不适用于性能关键型软件。 - John

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)

编辑 在阅读 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所问的那样,您并不需要一次获取所有内容,这是获得速度和内存的最佳方法。


你尝试过这些建议中的任何一个来查看它们是否对运行时间有任何影响吗? - Gareth Rees
在阅读了Gareth的答案后,我做到了。请查看我的回答中的编辑。 - Joël
@Gareth 噢,抱歉,我错过了你要求更多材料的请求。回答晚了,抱歉,我花了些时间进行格式化。 - Joël
顶部仍存在一些错误:“您正在复制数据两次”,以及有关可变和不可变对象的某些混淆。除此之外,看起来很好。 - Gareth Rees
哦,我明白了:我没有考虑到csv模块会返回每行的值列表。所以,我的回答开头有点跑题了。顺便问一下,test1不会返回一个引用列表,所有引用都指向csvReader解析的最后一个row列表吗? - Joël

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接