[Edu-sig] Some design patterns with iterators

Kirby Urner urnerk@qwest.net
Sun, 30 Mar 2003 13:06:48 -0800


The construct:

for i in collection:
    do something with i

is extremely common.  The collection is most simply a list or tuple,
but may in fact be any iterable object.  So the construct might be
rewritten:

for i in iterable:              # construct (1)
    do something with i

But how do we ensure that this is not an infinite loop?  I.e. the
collection returned by iterable may be inherently open-ended, e.g.
the sequence of fibonacci numbers 1 1 2 3 5 8 13...

A common modification to deal with such cases is:

for i in iterable:              # construct (2)
    if test(i): break
    do something with i

An alternative to the above is to return to the original construct (1),
but have the iterable become self-terminating.  The syntax:

iterable = iter(obj,sentinel)

uses the builtin iter() to compare a callable to a sentinel, and
stops iterating when they match.  So lets define an object that
returns the next fibonacci number whenever called, and which
internalizes a test, which, if positive, will return the sentinel
value:

----
# iters.py

class Fib(object):
     def __init__(self,start=0,next=1,test=False,sentinel=-1):
         self.start    = start
         self.next     = next
         self.sentinel = sentinel
         self.test     = test
     def __call__(self):
         self.start,self.next = self.next, self.start + self.next
         if self.test(self.start): return self.sentinel
         return self.start

def t(x):
     """x > 1000"""
     return x > 1000

fibobj = Fib(test=t)
iterable = iter(fibobj,-1)
for i in iterable:
    print i,
----

If you run the above script you'll get a listing of Fibonacci numbers less than
or equal to 1000:

Sun 03/30/2003 11:57:21.79
D:\Program Files\Python22>python ./work/iters.py
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Why go to all the trouble of passing in a test function, if we can simply set
a sentinel value to abort iteration?  The Fibonacci example suggests the 
answer:
we don't know in advance exactly what the collection will be (or think of
prime numbers), so we're not in a position to use a simple value to abort
iteration -- we need something like a comparison, or a more complicated set
of criteria, which is why we pass in a test.

A callable object, turned into an iterator by way of iter(), behaves much the
same as a generator object -- which objects may be functions.  The Fib object
might be rewritten as a generator, still accepting a test function as a
parameter:

  >>> def fibfunc(test):
         data = [1,1]
         while not test(data[0]):
            yield data[0]
            data[0],data[1] = data[1],data[0]+data[1]
         return

  >>> iterable = fibfunc(t)  # t is a test function
  >>> for i in iterable: print i,

  1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

In this case, fibfunc automatically returns an iterable.

What is the advantage of passing a test into an iterable, versus simply going
with construct (2)?   From a readability point of view, this approach has the
advantage of binding the test to the iterable at the time of definition.  The
same for-loop might handle lots of iterators, and needn't worry about which 
test
to use.  The test logically belongs with the iterator.

For example, let's modify iters.py (keeping original Fib object intact):

----
def testfunc(max):
     """a factory for test functions"""
     def t(x):
         return x > max
     t.__doc__ = "stop when x > %s" % max
     return t

t1 = testfunc(1000)
t2 = testfunc(5000)

def looper(iterable):
     # generic loop, doesn't worry about when to break
     for i in iterable:
         print i,
     print

print t1.__doc__
looper(iter(Fib(test=t1),-1))
print t2.__doc__
looper(iter(Fib(test=t2),-1))
----

  >>> reload(iters)
  stop when x > 1000
  1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
  stop when x > 5000
  1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181
  <module 'iters' from 'D:\Program Files\Python22\work\iters.py'>

In this case, the same test functions will work with the generator version.
Example:

  >>> iterable = fibfunc(t2)
  >>> for i in iterable: print i,

  1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181


--- Kirby