Iterators

A for loop is primarily used to traverse a list, but it picks the elements of the list one at a time. In particular, there is no need to store the whole list in memory for the loop to work properly. The mechanism that allows for loops to work without lists is that of iterators.

An iterable object produces objects (to be passed to a for loop). Such an object, obj, may be used inside a for loop, as follows:

for element in obj:
    ...

The notion of iterator thus generalizes the idea of lists. The simplest example of an iterable object is given by lists. The produced objects are simply the objects stored in the list:

L = ['A', 'B', 'C']
for element in L:
    print(element)

An iterable object need not produce existing objects. The objects may, instead, be produced on the fly.

A typical iterable is the object returned by the function range. This function works as if it would generate a list of integers, but instead, the successive integers are produced on the fly when they are needed:

for iteration in range(100000000):
    # Note: the 100000000 integers are not created at once
    if iteration > 10:
        break

If one really needs a list with all integers between 0 and 100,000,000, then it has to be formed explicitly:

l=list(range(100000000))

Generators

You can create your own iterators using the yield keyword. For example, a generator for odd numbers smaller than  n can be defined:

def odd_numbers(n):
    "generator for odd numbers less than n"
    for k in range(n):
        if k % 2 == 1:
            yield k

Then you can use it as follows:

g = odd_numbers(10)
for k in g:
    ...    # do something with k

Or even like this:

for k in odd_numbers(10):
    ... # do something with k

Iterators are disposable

One salient feature of iterators is that they may be used only once. In order to use the iterator again, you will have to create a new iterator object. Note that an iterable object is able to create new iterators as many times as necessary. Let us examine the case of a list:

L = ['a', 'b', 'c']
iterator = iter(L)
list(iterator) # ['a', 'b', 'c']
list(iterator) # [] empty list, because the iterator is exhausted

new_iterator = iter(L) # new iterator, ready to be used
list(new_iterator) # ['a', 'b', 'c']

Each time a generator object is called, it creates a new iterator. Hence, when that iterator is exhausted, one has to call the generator again to obtain a new iterator:

g = odd_numbers(10)
for k in g:
    ... # do something with k

# now the iterator is exhausted:
for k in g: # nothing will happen!!
    ...

# to loop through it again, create a new one:
g = odd_numbers(10)
for k in g:.
    ...

Iterator tools

Here are a couple of iterator tools that often come in very handy:

  • enumerate is used to enumerate another iterator. It produces a new iterator that yields pairs (iteration, element), where iteration stores the index of the iteration:
      A = ['a', 'b', 'c']
      for iteration, x in enumerate(A):
          print(iteration, x)
      # result: (0, 'a') (1, 'b') (2, 'c')
  • reversed creates an iterator from a list by going through that list backwards. Notice that this is different from creating a reversed list:
      A = [0, 1, 2]
      for elt in reversed(A):,
          print(elt)
          # result: 2 1 0
  • itertools.count is a possibly infinite iterator of integers:
      for iteration in itertools.count():
          if iteration > 100:
              break # without this, the loop goes on forever
          print("integer {}".format(iteration))
          # prints the 100 first integer
  • intertools.islice truncates an iterator using the familiar slicing syntax; refer to Chapter 3, Container Types. One application is creating a finite iterator from an infinite one:
      from itertools import count, islice
      for iteration in islice(count(), 10): 
          # same effect as range(10)
          ...

For example, let's find some odd numbers by combining islice with an infinite generator. First, we modify the generator for odd numbers so that it becomes an infinite generator:

def odd_numbers():
    k=-1
    while True:
        k+=1
        if k%2==1:
        yield k

Then, we use it with islice to get a list of some odd numbers:

list(itertools.islice(odd_numbers(),10,30,8)) # returns [21, 37, 53]

Generators of recursive sequences

Assume that a sequence is given by an induction formula. For instance, consider the Fibonacci sequence, defined by the recurrence formula: un = un-1 + un-2.

This sequence depends on two initial values, namely u0 and u1, although for the standard Fibonacci sequence those numbers are taken as 0 and 1 respectively. A nifty way of programming the generation of such a sequence is by using generators, as follows:

def fibonacci(u0, u1):
    """
    Infinite generator of the Fibonacci sequence.
    """
    yield u0
    yield u1
    while True:
        u0, u1 = u1, u0+u1
        yield u1

This may then be used, for instance, like this:

# sequence of the 100 first Fibonacci numbers:
list(itertools.islice(fibonacci(0, 1), 100))

 Arithmetic geometric mean

The iteration based on iteratively computing arithmetic and geometric means is called AGM iteration (refer to [1, p. 598] for more information):

 Arithmetic geometric mean

It has the fascinating property that for   Arithmetic geometric mean :

 Arithmetic geometric mean

The integral on the right-hand side is called a complete elliptic integral of the first kind. We now proceed to compute this elliptic integral. We use a generator to describe the iteration:

def arithmetic_geometric_mean(a, b):
    """
    Generator for the arithmetic and geometric mean
    a, b initial values
    """ 
    while True:    # infinite loop
         a, b = (a+b)/2, sqrt(a*b)
         yield a, b

As the sequence {ai} is convergent, the sequence {ci}  defined by {ci} = (ai–bi)/2, converges to zero – a fact that will be used to terminate the iteration in the program to compute the elliptic integral:

def elliptic_integral(k, tolerance=1e-5):
    """
    Compute an elliptic integral of the first kind.
    """
    a_0, b_0 = 1., sqrt(1-k**2)
    for a, b in arithmetic_geometric_mean(a_0, b_0):
        if abs(a-b) < tolerance:
            return pi/(2*a)

We have to make sure that the algorithm stops. Note that this code fully relies on the mathematical statement that the arithmetic geometric mean iteration converges (fast). In practical computing, we have to be careful while applying theoretical results, as they might no longer be valid in limited-precision arithmetic. The right way to make the preceding code safe is to use itertools.islice. The safe code is as follows (see the example under the section Controlling the flow inside the loop for another typical usage of the for/else statement):

from itertools import islice
def elliptic_integral(k, tolerance=1e-5, maxiter=100):
    """
    Compute an elliptic integral of the first kind.
    """
    a_0, b_0 = 1., sqrt(1-k**2)
    for a, b in islice(arithmetic_geometric_mean(a_0, b_0), 
                                                  maxiter):
        if abs(a-b) < tolerance:
            return pi/(2*a)
    else:
        raise Exception("Algorithm did not converge")

As an application, elliptic integrals may be used to compute the period of a pendulum of length L starting at an angle θ (refer to [18, p.114] for more information) using:

 Arithmetic geometric mean

Using this formula, the period of the pendulum is easily obtained:

def pendulum_period(L, theta, g=9.81):
    return 4*sqrt(L/g)*elliptic_integral(sin(theta/2))
..................Content has been hidden....................

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