views:

5295

answers:

4

Code like this often happens:

l = []
while foo:
    #baz
    l.append(bar)
    #qux

This is really slow if you're about to append thousands of elements to your list, as the list will have to constantly be re-initialized to grow. (I understand that lists aren't just wrappers around some array-type-thing, but something more complicated. I think this still applies, though; let me know if not).

In Java, you can create an ArrayList with an initial capacity. If you have some idea how big your list will be, this will be a lot more efficient.

I understand that code like this can often be re-factored into a list comprehension. If the for/while loop is very complicated, though, this is unfeasible. Is there any equivalent for us python programmers?

+10  A: 

Python lists have no built-in pre-allocation. If you really need to make a list, and need to avoid the overhead of appending (and you should verify that you do), you can do this:

l = [None] * 1000 # Make a list of 1000 None's
for i in xrange(1000):
    # baz
    l[i] = bar
    # qux

Perhaps you could avoid the list by using a generator instead:

def my_things():
    while foo:
        #baz
        yield bar
        #qux

for thing in my_things():
    # do something with thing

This way, the list isn't every stored all in memory at all, merely generated as needed.

Ned Batchelder
+1 Generators instead of lists. Many algorithms can be revised slightly to work with generators instead of full-materialized lists.
S.Lott
generators are a good idea, true. i was wanting a general way to do it besides the setting in-place. i guess the difference is minor, thoguh.
Claudiu
A: 

From what I understand, python lists are already quite similar to ArrayLists. But if you want to tweak those parameters I found this post on the net that may be interesting (basically, just create your own ScalableList extension):

http://mail.python.org/pipermail/python-list/2000-May/035082.html

Piotr Lesnicki
+19  A: 
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

Results. (evaluate each function 144 times and average the duration)

simple append 0.0102
pre-allocate  0.0098

Conclusion. It barely matters.

Premature optimization is the root of all evil.

S.Lott
fair enough! i did a similar test and found a difference of 1.00 vs 0.8 or so. I guess it doesn't matter so much.
Claudiu
I agree 100% with this answer. The only thing I would add is that you may get some lift out of using generator expressions like: list(i for i in xrange(size))
Jeremy Cantrell
@Jeremy Cantrell: Feel free to post your results for comparison.
S.Lott
What if the preallocation method (size*[None]) itself is inefficient? Does the python VM actually allocate the list at once, or grow it gradually, just like the append() would?
haridsv
@haridsv: "What if the preallocation method (size*[None]) itself is inefficient?" What does that mean? It's equally efficient. It's not inefficient. It's the same.
S.Lott
What I was wondering is that if python preallocates the list with the final size or will only grow it on demand (ie., attempt to grow "size" number of times, just like for the regular append()). Just considering all corners... it is simple enough to optimize so it is quite likely that this case is already optimized, but remains an assumption unless someone knows the internals for sure.
haridsv
@haridsv: (1) I can't understand what hair you're splitting. The time is the same. I don't know what other "optimization" you could be talking about. (2) The internals are trivially available -- you can read the source at any time.
S.Lott
@haridsv: I made some test by putting the allocation (`result=size*[None]`) outside the function and can't find a significant difference. By replacing the loop body by `result[i] = i`, I then get 1.13 ms versus 0.62 ms with preallocation.
rafak
@S.Lott: What haridsv is saying is that if int*list is implemented by growing a list one element at a time, then it's no wonder both implementations shown above take the same time. If this were the case, then it is possible that pre-allocating might be much faster - but we haven't yet found any code to test that case.
Tartley
@Tartley: "haven't yet found any code to test that case". Baffling. If it can't be expressed in Python, then what are we talking about?
S.Lott
Hey. It presumably can be expressed in Python, but nobody has yet posted it here. haridsv's point was that we're just assuming 'int * list' doesn't just append to the list item by item. That assumption is probably valid, but haridsv's point was that we should check that. If it wasn't valid, that would explain why the two functions you showed take almost identical times - because under the covers, they are doing exactly the same thing, hence haven't actually tested the subject of this question. Best regards!
Tartley
+1  A: 

i ran @s.lott's code and produced the same 10% perf increase by pre-allocating. tried @jeremy's idea using a generator and was able to see the perf of the gen better than that of the doAllocate. For my proj the 10% improvement matters, so thanks to everyone as this helps a bunch.

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
Jason Wiener
"For my proj the 10% improvement matters"? Really? You can **prove** that list allocation is **the** bottleneck? I'd like to see more on that. Do you have a blog where you could explain how this actually helped?
S.Lott