Using collections

The collections module provides high-performance alternatives to the built-in container types. The main types available in this module are:

  • deque: A list-like type with extra features
  • defaultdict: A dict-like type with a built-in default factory feature
  • namedtuple: A tuple-like type that assigns keys for members

deque

A deque is an alternative implementation for lists. While a list is based on arrays, a deque is based on a doubly linked list. Hence, a deque is much faster when you need to insert something into its middle or head but much slower when you need to access an arbitrary index.

Of course, thanks to the overallocation of an internal array in the Python list type, not every list.append() call requires memory reallocation, and the average complexity of this method is O(1). Still, pops and appends are generally faster when performed on linked lists instead of arrays. The situation changes dramatically when the element needs to be added on arbitrary point of sequence. Because all elements on the right of the new one need to be shifted in an array, the complexity of list.insert() is O(n). If you need to perform a lot of pops, appends, and inserts, the deque in place of the list may provide substantial performance improvement. But always be sure to profile your code before switching from a list to the deque, because a few things that are fast in arrays (such as accessing arbitrary index) are extremely inefficient in linked lists.

For example, if we measure the time of appending one element and removing it from the sequence with timeit, the difference between list and deque may not even be noticeable:

$ python3 -m timeit 
> -s 'sequence=list(range(10))' 
> 'sequence.append(0); sequence.pop();'
1000000 loops, best of 3: 0.168 usec per loop
$ python3 -m timeit  
> -s 'from collections import deque; sequence=deque(range(10))' 
> 'sequence.append(0); sequence.pop();'
1000000 loops, best of 3: 0.168 usec per loop

But if we do similar comparison for situations when we want to add and remove the first element of the sequence, the performance difference is impressive:

$ python3 -m timeit 
> -s 'sequence=list(range(10))' 
> 'sequence.insert(0, 0); sequence.pop(0)'

1000000 loops, best of 3: 0.392 usec per loop
$ python3 -m timeit 
> -s 'from collections import deque; sequence=deque(range(10))' 
> 'sequence.appendleft(0); sequence.popleft()'
10000000 loops, best of 3: 0.172 usec per loop

And the difference is, it gets bigger when the size of the sequence grows. Here is an example of the same test performed on lists containing 10,000 elements:

$ python3 -m timeit 
> -s 'sequence=list(range(10000))' 
> 'sequence.insert(0, 0); sequence.pop(0)'
100000 loops, best of 3: 14 usec per loop
$ python3 -m timeit 
> -s 'from collections import deque; sequence=deque(range(10000))'  
> 'sequence.appendleft(0); sequence.popleft()'
10000000 loops, best of 3: 0.168 usec per loop

Thanks to efficient append() and pop() methods that work at the same speed from both ends of the sequence, deque makes a perfect type for implementing queues. For example, a FIFO (First In First Out) queue will definitely be much more efficient if implemented with a deque instead of list.

Note

deque works great when implementing queues. Anyway, starting from Python 2.6 there is a separate queue module in Python's standard library that provides basic implementation for FIFO, LIFO, and priority queues. If you want to utilize queues as a mechanism of interthread communication, you should really use classes from the queue module instead of collections.deque. This is because these classes provide all the necessary locking semantics. If you don't use threading and don't utilize queues as a communication mechanism, then deque should be enough to provide queue implementation basics.

defaultdict

The defaultdict type is similar to the dict type but adds a default factory for new keys. This avoids writing an extra test to initialize the mapping entry and is more efficient than the dict.setdefault method.

defaultdict seems just like syntactic sugar over dict that simply allows you to write shorter code. In fact, the fallback to a predefined value on a failed key lookup is also slightly faster than the dict.setdefault() method:

$ python3 -m timeit 
> -s 'd = {}' 
> 'd.setdefault("x", None)'
10000000 loops, best of 3: 0.153 usec per loop
$ python3 -m timeit  
> -s 'from collections import defaultdict; d=defaultdict(lambda: None)' 
> 'd["x"]'
10000000 loops, best of 3: 0.0447 usec per loop

The difference isn't great because the computational complexity hasn't changed. The dict.setdefault method consist of two steps (key lookup and key set), both of which have a complexity of O(1), as we have seen in the Dictionaries section in Chapter 2, Syntax Best Practices – below the Class Level. There is no way to have a complexity class lower than O(1). But it is indisputably faster in some situations and it is worth knowing because every small speed improvement counts when optimizing critical code sections.

The defaultdict type takes a factory as a parameter and can therefore be used with built-in types or classes whose constructor does not take arguments. Here is an example from the official documentation that shows how to use defaultdict for counting:

>>> s = 'mississippi'
>>> d = defaultdict(int)
>>> for k in s:
...     d[k] += 1
...
>>> list(d.items())
[('i', 4), ('p', 2), ('s', 4), ('m', 1)]

namedtuple

namedtuple is a class factory that takes a type name and a list of attributes and creates a class out of it. The class can then be used to instantiate a tuple-like object and provide accessors for its elements:

>>> from collections import namedtuple 
>>> Customer = namedtuple(
...     'Customer',
...     'firstname lastname'
... )
>>> c = Customer('Tarek', 'Ziadé')
>>> c.firstname
'Tarek'

It can be used to create records that are easier to write compared to a custom class that would require some boilerplate code to initialize values. On the other hand, it is based on tuple, so access to its elements by index is very fast. The generated class can be subclassed to add more operations.

The gain from using namedtuple instead of other datatypes may not be obvious at first. The main advantage is that it is way more easier to use, understand, and interpret than ordinary tuples. Tuple indexes don't carry any semantics, so it is great to access tuple elements by attributes too. However, you could get the same benefit from dictionaries that have an O(1) average complexity of get/set operations.

The first advantage in terms of performance is that namedtuple is still the flavor of tuple. It means that it is immutable, so the underlying array storage is allocated exactly for the needed size. Dictionaries, on the other hand, need to use overallocation of the internal hash table to ensure low average complexity of get/set operations. So, namedtuple wins over dict in terms of memory efficiency.

The fact that namedtuple is based on a tuple may also be beneficial for performance. Its elements may be accessed by an integer index, like in two other simple sequence objects—lists and tuples. This operation is both simple and fast. In the case of dict or custom class instances (that also use dictionaries for storing attributes), the element access requires hash table lookup. It is highly optimized to ensure good performance independently from collection size, but the mentioned O(1) complexity is actually only the average complexity. The actual, amortized worst case complexity for set/get operations in dict is O(n). The real amount of work when performing such an operation at a given moment is dependent on both collection size and its history. So, in sections of code that are critical for performance, sometimes it may be wise to use lists or tuples instead of dictionaries. This is only because they are more predictable when it comes to performance.

In such a situation, namedtuple is a great type that combines the advantages of dictionaries and tuples:

  • In sections where readability is more important, the attribute access may be preferred
  • In performance-critical sections, elements may be accessed by their indexes

Note

Reduced complexity can be achieved by storing the data in an efficient data structure that works well with the way the algorithm will use it.

That said, when the solution is not obvious, you should consider dropping and rewriting the incriminated part instead of killing the code readability for the sake of performance.

Often, the Python code can be both readable and fast. So, try to find a good way to perform the work instead of trying to work around a flawed design.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset