List filling patterns

In this section we will compare different ways to fill lists. They are different in computational  efficiency and also in code readability.

List filling with the append method

A ubiquitous programming pattern is to compute elements and store them in a list:

L = []
for k in range(n):
    # call various functions here
    # that compute "result"
    L.append(result)

This approach has a number of disadvantages:

  • The number of iterations is decided in advance. If there is a break instruction, then the preceding code takes care of both generating values and deciding when to stop. This is not desirable and lacks flexibility.
  • It makes the assumption that the user wants the whole history of the computation, for all the iterations. Suppose we are only interested in the sum of all the computed values. If there are many computed values, it does not make sense to store them, as it is much more efficient to add them one at a time.

List from iterators

Iterators provide us with an elegant solution to problems discussed previously:

def result_iterator():
    for k in itertools.count(): # infinite iterator
        # call various functions here
        # that compute "result"
        ...
        yield result

With iterators, we separate the task of generating the computed values without bothering about the stopping condition or about the storage. If the user of that code wants to store the n first values, it is easily done using the list constructor:

L = list(itertools.islice(result_iterator(), n)) # no append needed!

If the user wants the sum of the first n generated values, this construction is recommended:

# make sure that you do not use scipy.sum here
s = sum(itertools.islice(result_iterator(), n))

What we did here was separating the generation of elements on one hand, and storing those elements on the other.

If the purpose is really to build a list, and when the result at each step does not depend on previously computed elements, one may use the list comprehension syntax (refer to section List of Chapter 3, Container Types, for more information):

L = [some_function(k) for k in range(n)]

When iteratively computing values that depend on the previously computed values, list comprehensions cannot help.

Storing generated values

Using iterators to fill out lists will work nicely most of the time, but there are complications to this pattern when the algorithm computing the new values is liable to throw an exception; if the iterator raises an exception along the way, the list will not be available! The following example illustrates this problem.

Suppose we generate the sequence defined recursively by Storing generated values . This sequence quickly diverges to infinity if the initial data u0 is greater than one. Let us generate it with a generator:

import itertools
def power_sequence(u0):
    u = u0
    while True:
        yield u
        u = u**2

If you try to obtain the first 20 elements of the sequence (initialized by u0 = 2) by executing,

list(itertools.islice(power_sequence(2.), 20))

an exception will be raised and no list will be available, not even the list of elements before the exception was raised. There is currently no way to obtain a partially filled list from a possibly faulty generator. The only way around is to use the append method wrapped in an exception-catching block (refer to section Exceptions in Chapter 10, Error Handling, for more details):

generator = power_sequence(2.)
L = []
for iteration in range(20):
    try:
        L.append(next(generator))
    except Exception:
        ...
..................Content has been hidden....................

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