Sunday 13 December 2015

Weekly Python #9 - Adding to a list - only one way ?

Adding to a list

Is there really only one way ?

It is one of the principles of python that "There should be one-- and preferably only one --obvious way to do it." You will notice though when it comes to many of the parts of the standard language, that there is actually many different ways to do somethings - for instance adding a single item to a list.
>>> a = [0,1,2,3,4,5,6,7,8,9,10]
>>> a.append(11)
>>> a += [12]
>>> a = a + [13]
>>> a.extend([14])
>>> a
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]
All of these appear to do the same things, but behind the scenes they all do different things - and have different advantages.

a.append 

Using the append method is by far the most obvious method : It is very readable (and readability is key when writing software). It is implemented a single method call on the list method, and once the append is complete there is no return value. it all makes for a lightning quick execution
$ python -m timeit -n 1000000 -s 'a = [i for i in range(1000)]' 'a.append(1001)'
1000000 loops, best of 3: 0.0452 usec per loop
0.0452 micro seconds (a micro second is one millionth of a second) to append 1,000,000 elements to a list.

Using +=

For some people this idiom is just as readable as an append but it is actually a bit less efficient, as there a few things that have to be done before the item can be appended to the list.
  • the element (12 in our case) has to be made into a list
  • The __iadd__ method is called
  • The __iadd__ method does a type check to ensure that the argument is a list
$ python -m timeit -n 1000000 -s 'a = [i for i in range(1000)]' 'a += [1001]'
1000000 loops, best of 3: 0.081 usec per loop
Using the += method takes nearly 2 times longer than using append for adding single elements.

It would probably be far more efficient to use this method to add another existing list (rather than looping around the list and appending each element) - we will look at this later

Using + 

A first glance doing a = a + [13] is going to be similar in performance to the previous statement a += [12] but there is a lot more going under the skin here.
  • the element (12 in our case) has to be made into a list
  • The __add__ method is called
  • The __add__ method does a type check to ensure that the argument is a list
  • The __add__ method creates a brand new empty list to hold the result of the addition, and will need to copy the original a list and argument list into this new empty list. The name a will be bound to the new list returned by the __add__ method.
These difference go some way to explaining to performance differences :
$ python -m timeit -n 1000000 -s 'a = [i for i in range(1000)]' 'a = a + [1001]'
10000 loops, best of 3: 25.4 usec per loop
So using a straight list addition as above - it takes over 500 times longer to add a single element to a list; and it can't be recommended for use. It will also use more memory (as it creates this brand new list and fills it, before the old one is unbound), and this could also be significant for your application.

Note

This significant performance impact seems to not be connected to the size of the original list - even adding a single element to an empty list takes a similar amount of time, and creation of a new empty list doesn't seem to be the issue either. At the time of writing the performance impact seems to be caused by the re-binding of the new list to one of the names in the expression.

The only advantage is that if you have another name bound to the original list, then that original list wont be changed - but there are better ways to ensure that this happens - by taking a copy of the original list before you change it :
>>> a = [0,1,2,3,4,5,6,7,8,9,10]
>>> b = a[:]            # Take an explicit shallow copy of a
>>> a.append(11)        # Use append - which we know is a lot quicker
>>> a, b
[0,1,2,3,4,5,6,7,8,9,10,11], [0,1,2,3,4,5,6,7,8,9,10]
>>>

extend

The extend method is intended to be used to combine two lists together; a.extend(x) is equivalent to a += x (where a and x are both lists). We can also use extend to add single elements to a list :
$ python -m timeit -s 'a = [i for i in range(1000)]' 'a.extend([1001])'
10000000 loops, best of 3: 0.0924 usec per loop
So extend is approximately equivalent to using a += x - as expected.

Adding multiple elements efficiently

So far we have looked at adding single elements to a list, but often our code needs combine two lists; so it would be useful to look at the relative performance of a += x vs a.extend(x} where x is a list.
$ python -m timeit -s 'a = [i for i in range(1000)]' -s 'b=[i for i in range(1001,2001)]' 'a += b'
100000 loops, best of 3: 2.25 usec per loop
$ python -m timeit -s 'a = [i for i in range(1000)]' -s 'b=[i for i in range(1001,2001)]' 'a.extend(b)'
100000 loops, best of 3: 2.3 usec per loop
So within a margin of error the two are roughly equivalent. Just out of interest how much slower is appending each element :
$ python -m timeit -s 'a = [i for i in range(1000)]' -s 'b=[i for i in range(1001,2001)]' 
'for e in b:' '   a.append(e)'
10000 loops, best of 3: 47.2 usec per loop
Appending each element of a 1000 element list is 23 times slower than adding the two lists together, and it is not unreasonable to expect that the gap between the two approaches will increase as the lists get longer.

Conclusion

Although (on the face of things) there are 4 different methods for adding an element to a list, and at least 3 for combining lists together, there is really only one way that you should use; append for single items and extend of combining lists. The guiding "One obvious way" principle espoused by Python experts still holds, although sometimes it isn't obvious which way is the obvious one.

No comments:

Post a Comment