这样的代码经常发生:
l = []
while foo:
#baz
l.append(bar)
#qux
如果您要将数千个元素追加到列表中,这非常慢,因为必须不断调整列表大小以适应新元素。
在Java中,您可以创建具有初始容量的ArrayList。如果您对列表的大小有所了解,那么效率会更高。
我知道像这样的代码通常可以重新考虑到列表理解中。但是,如果for / while循环非常复杂,那么这是不可行的。我们的Python程序员有没有相同的东西?
答案 0 :(得分:116)
def doAppend( size=10000 ):
result = []
for i in range(size):
message= "some unique object %d" % ( i, )
result.append(message)
return result
def doAllocate( size=10000 ):
result=size*[None]
for i in range(size):
message= "some unique object %d" % ( i, )
result[i]= message
return result
<强>结果。 (评估每个功能144次并平均持续时间)
simple append 0.0102
pre-allocate 0.0098
<强>结论即可。这几乎不重要。
过早优化是万恶之源。
答案 1 :(得分:68)
Python列表没有内置的预分配。如果你真的需要制作一个列表,并且需要避免附加的开销(你应该验证你这样做),你可以这样做:
l = [None] * 1000 # Make a list of 1000 None's
for i in xrange(1000):
# baz
l[i] = bar
# qux
也许您可以通过使用生成器来避免列表:
def my_things():
while foo:
#baz
yield bar
#qux
for thing in my_things():
# do something with thing
这样,列表并不是全部存储在内存中,只是根据需要生成。
答案 2 :(得分:43)
简短版:使用
pre_allocated_list = [None] * size
预先分配一个列表(也就是说,能够解决列表的'size'元素,而不是通过追加逐渐形成列表)。即使在大型列表中,此操作也非常快。分配稍后将分配给列表元素的新对象将花费更长时间,并且将成为程序中的瓶颈,性能方面。
长版:
我认为应该考虑初始化时间。 因为在python中,一切都是引用,无论你是将每个元素设置为None还是一些字符串都无关紧要 - 无论哪种方式,它都只是一个引用。虽然如果你想为每个要引用的元素创建新对象需要更长的时间。
对于Python 3.2:
import time
import copy
def print_timing (func):
def wrapper (*arg):
t1 = time.time ()
res = func (*arg)
t2 = time.time ()
print ("{} took {} ms".format (func.__name__, (t2 - t1) * 1000.0))
return res
return wrapper
@print_timing
def prealloc_array (size, init = None, cp = True, cpmethod=copy.deepcopy, cpargs=(), use_num = False):
result = [None] * size
if init is not None:
if cp:
for i in range (size):
result[i] = init
else:
if use_num:
for i in range (size):
result[i] = cpmethod (i)
else:
for i in range (size):
result[i] = cpmethod (cpargs)
return result
@print_timing
def prealloc_array_by_appending (size):
result = []
for i in range (size):
result.append (None)
return result
@print_timing
def prealloc_array_by_extending (size):
result = []
none_list = [None]
for i in range (size):
result.extend (none_list)
return result
def main ():
n = 1000000
x = prealloc_array_by_appending(n)
y = prealloc_array_by_extending(n)
a = prealloc_array(n, None)
b = prealloc_array(n, "content", True)
c = prealloc_array(n, "content", False, "some object {}".format, ("blah"), False)
d = prealloc_array(n, "content", False, "some object {}".format, None, True)
e = prealloc_array(n, "content", False, copy.deepcopy, "a", False)
f = prealloc_array(n, "content", False, copy.deepcopy, (), False)
g = prealloc_array(n, "content", False, copy.deepcopy, [], False)
print ("x[5] = {}".format (x[5]))
print ("y[5] = {}".format (y[5]))
print ("a[5] = {}".format (a[5]))
print ("b[5] = {}".format (b[5]))
print ("c[5] = {}".format (c[5]))
print ("d[5] = {}".format (d[5]))
print ("e[5] = {}".format (e[5]))
print ("f[5] = {}".format (f[5]))
print ("g[5] = {}".format (g[5]))
if __name__ == '__main__':
main()
评价为:
prealloc_array_by_appending took 118.00003051757812 ms
prealloc_array_by_extending took 102.99992561340332 ms
prealloc_array took 3.000020980834961 ms
prealloc_array took 49.00002479553223 ms
prealloc_array took 316.9999122619629 ms
prealloc_array took 473.00004959106445 ms
prealloc_array took 1677.9999732971191 ms
prealloc_array took 2729.999780654907 ms
prealloc_array took 3001.999855041504 ms
x[5] = None
y[5] = None
a[5] = None
b[5] = content
c[5] = some object blah
d[5] = some object 5
e[5] = a
f[5] = []
g[5] = ()
正如您所看到的,只需创建一个对同一None对象的引用的大列表,只需要很少的时间。
前置或延长需要更长的时间(我没有做任何平均值,但经过几次运行后我可以告诉你,延伸和追加大致需要相同的时间)。
为每个元素分配新对象 - 这是花费最多时间的事情。而S.Lott的答案就是这样 - 每次都会格式化一个新的字符串。这不是严格要求的 - 如果您想预先分配一些空间,只需创建一个None列表,然后随意将数据分配给列表元素。无论哪种方式,生成数据都需要花费更多时间而不是追加/扩展列表,无论是在创建列表时生成还是在生成列表之后生成数据。但是如果你想要一个人口稀少的列表,那么以None列表开头肯定会更快。
答案 3 :(得分:21)
Pythonic的方法是:
x = [None] * numElements
或您希望预先填写的任何默认值,例如
bottles = [Beer()] * 99
sea = [Fish()] * many
vegetarianPizzas = [None] * peopleOrderingPizzaNotQuiche
[编辑:警告Emptor [Beer()] * 99
语法创建一个 Beer
然后填充一个数组,其中包含对同一个实例的99个引用]
Python的默认方法非常有效,尽管随着元素数量的增加,效率会下降。
比较
import time
class Timer(object):
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
end = time.time()
secs = end - self.start
msecs = secs * 1000 # millisecs
print('%fms' % msecs)
Elements = 100000
Iterations = 144
print('Elements: %d, Iterations: %d' % (Elements, Iterations))
def doAppend():
result = []
i = 0
while i < Elements:
result.append(i)
i += 1
def doAllocate():
result = [None] * Elements
i = 0
while i < Elements:
result[i] = i
i += 1
def doGenerator():
return list(i for i in range(Elements))
def test(name, fn):
print("%s: " % name, end="")
with Timer() as t:
x = 0
while x < Iterations:
fn()
x += 1
test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)
与
#include <vector>
typedef std::vector<unsigned int> Vec;
static const unsigned int Elements = 100000;
static const unsigned int Iterations = 144;
void doAppend()
{
Vec v;
for (unsigned int i = 0; i < Elements; ++i) {
v.push_back(i);
}
}
void doReserve()
{
Vec v;
v.reserve(Elements);
for (unsigned int i = 0; i < Elements; ++i) {
v.push_back(i);
}
}
void doAllocate()
{
Vec v;
v.resize(Elements);
for (unsigned int i = 0; i < Elements; ++i) {
v[i] = i;
}
}
#include <iostream>
#include <chrono>
using namespace std;
void test(const char* name, void(*fn)(void))
{
cout << name << ": ";
auto start = chrono::high_resolution_clock::now();
for (unsigned int i = 0; i < Iterations; ++i) {
fn();
}
auto end = chrono::high_resolution_clock::now();
auto elapsed = end - start;
cout << chrono::duration<double, milli>(elapsed).count() << "ms\n";
}
int main()
{
cout << "Elements: " << Elements << ", Iterations: " << Iterations << '\n';
test("doAppend", doAppend);
test("doReserve", doReserve);
test("doAllocate", doAllocate);
}
在我的Windows 7 i7上,64位Python提供
Elements: 100000, Iterations: 144
doAppend: 3587.204933ms
doAllocate: 2701.154947ms
doGenerator: 1721.098185ms
虽然C ++给出了(使用MSVC构建,64位,启用了优化)
Elements: 100000, Iterations: 144
doAppend: 74.0042ms
doReserve: 27.0015ms
doAllocate: 5.0003ms
C ++调试版本产生:
Elements: 100000, Iterations: 144
doAppend: 2166.12ms
doReserve: 2082.12ms
doAllocate: 273.016ms
这里的要点是,使用Python可以实现7-8%的性能提升,如果您认为自己正在编写高性能应用程序(或者如果您正在编写用于Web服务的内容或那些不被嗤之以鼻,但你可能需要重新考虑你选择的语言。
此外,这里的Python代码并不是真正的Python代码。切换到真正的Pythonesque代码可以提供更好的性能:
import time
class Timer(object):
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
end = time.time()
secs = end - self.start
msecs = secs * 1000 # millisecs
print('%fms' % msecs)
Elements = 100000
Iterations = 144
print('Elements: %d, Iterations: %d' % (Elements, Iterations))
def doAppend():
for x in range(Iterations):
result = []
for i in range(Elements):
result.append(i)
def doAllocate():
for x in range(Iterations):
result = [None] * Elements
for i in range(Elements):
result[i] = i
def doGenerator():
for x in range(Iterations):
result = list(i for i in range(Elements))
def test(name, fn):
print("%s: " % name, end="")
with Timer() as t:
fn()
test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)
哪个给出了
Elements: 100000, Iterations: 144
doAppend: 2153.122902ms
doAllocate: 1346.076965ms
doGenerator: 1614.092112ms
(在32位doGenerator中比doAllocate更好。)
这里doAppend和doAllocate之间的差距要大得多。
显然,这里的差异实际上只适用于您执行此操作的次数超过几次,或者您是在负载较重的系统上执行此操作时,这些数字将按数量级进行扩展,或者如果您正在处理更大的名单。
这里的要点:以最佳表现的pythonic方式。
但是如果你担心一般的高级性能,Python就是错误的语言。由于Python功能(如装饰器等)(https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Data_Aggregation#Data_Aggregation),Python函数调用的最基本问题传统上比其他语言慢300倍。
答案 4 :(得分:8)
正如其他人所提到的,使用NoneType
个对象预先播种列表的最简单方法。
话虽如此,在决定必要之前,你应该理解Python列表实际工作的方式。在列表的CPython实现中,底层数组总是以开销空间创建,逐渐变大的大小( 4, 8, 16, 25, 35, 46, 58, 72, 88, 106, 126, 148, 173, 201, 233, 269, 309, 354, 405, 462, 526, 598, 679, 771, 874, 990, 1120, etc)
,因此调整列表大小几乎不会发生。
由于这种行为,大多数 list.append()
函数的附加复杂度为O(1)
,跨越其中一个边界只会增加复杂性,此时复杂性将是O(n)
。这种行为导致了S. Lott的答案中执行时间的最小增加。
来源:http://www.laurentluce.com/posts/python-list-implementation/
答案 5 :(得分:4)
我运行了@ s.lott的代码并通过预分配产生了相同的10%性能提升。尝试@ jeremy使用生成器的想法,并且能够比doAllocate更好地看到gen的性能。对于我的项目,10%的改进很重要,所以感谢大家,因为这有助于一堆。
def doAppend( size=10000 ):
result = []
for i in range(size):
message= "some unique object %d" % ( i, )
result.append(message)
return result
def doAllocate( size=10000 ):
result=size*[None]
for i in range(size):
message= "some unique object %d" % ( i, )
result[i]= message
return result
def doGen( size=10000 ):
return list("some unique object %d" % ( i, ) for i in xrange(size))
size=1000
@print_timing
def testAppend():
for i in xrange(size):
doAppend()
@print_timing
def testAlloc():
for i in xrange(size):
doAllocate()
@print_timing
def testGen():
for i in xrange(size):
doGen()
testAppend()
testAlloc()
testGen()
testAppend took 14440.000ms
testAlloc took 13580.000ms
testGen took 13430.000ms
答案 6 :(得分:3)
如果您正在使用numpy,那么会出现对Python中预分配的担忧,numpy具有更多类似C的数组。在这种情况下,预分配问题涉及数据的形状和默认值。
如果您在大量列表上进行数值计算并想要性能,请考虑numpy。
答案 7 :(得分:0)
对于某些应用程序,字典可能是您正在寻找的。例如,在find_totient方法中,我发现使用字典更方便,因为我没有零索引。
def totient(n):
totient = 0
if n == 1:
totient = 1
else:
for i in range(1, n):
if math.gcd(i, n) == 1:
totient += 1
return totient
def find_totients(max):
totients = dict()
for i in range(1,max+1):
totients[i] = totient(i)
print('Totients:')
for i in range(1,max+1):
print(i,totients[i])
使用预先分配的列表也可以解决此问题:
def find_totients(max):
totients = None*(max+1)
for i in range(1,max+1):
totients[i] = totient(i)
print('Totients:')
for i in range(1,max+1):
print(i,totients[i])
我觉得这不是那么优雅而且容易出错,因为我存储了None,如果我不小心使用它们就会抛出异常,并且因为我需要考虑地图让我避免的边缘情况
这本字典确实不会有效率,但正如其他人评论的那样,小速度差异并不总是值得重要维护危险。
答案 8 :(得分:-1)
据我所知,python列表已经与ArrayLists非常相似。但是如果你想调整这些参数我在网上发现了这篇可能很有趣的帖子(基本上只是创建你自己的ScalableList
扩展名):
http://mail.python.org/pipermail/python-list/2000-May/035082.html