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))
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
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:. ...
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]
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))
The iteration based on iteratively computing arithmetic and geometric means is called AGM iteration (refer to [1, p. 598] for more information):
It has the fascinating property that for :
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:
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))
3.139.97.202