#! /usr/bin/python3 ''' From Learning Python, Chapter 14 The concept of “iterable objects” is relatively recent in Python, but it has come to permeate the language’s design. It’s essentially a generalization of the notion of sequences—an object is considered iterable if it is either a physically stored sequence, or an object that produces one result at a time in the context of an iteration tool like a for loop. In a sense, iterable objects include both physical sequences and virtual sequences computed on demand. See also Python Cookbook, Chapter 4 ''' ## iterate over files def it0(file="iterators.py"): for line in open(file): print (line, end='') def it1(file="iterators.py"): f = open(file) print( f.readline()) print(f.readline()) print(f.__next__()) print(f.__next__()) print(next(f)) print(next(f)) f.close() ## __next__() and readline() are equivalent for files ## they each iterate over the file a line at a time. ## __next__() is an ITERATOR in Python ## the for loop calls __next__() ## next(f) calls the iterator for f ## Can explicitly get (or make) an iterator using iter() def it2(lst = [1,2,3,4]): it = iter(lst) print (it.__next__()) print (next(it)) print (next(it)) ## automatic iteration def it3(lst = [1,2,3,4]): for i in lst: print (i,' ', end='') ## manual iteration def it4(lst = [1,2,3,4]): it = iter(lst) while True: try: x = next(it) except StopIteration: break print (x, ' ', end='') ## other built-in iterables: dictionaries def it5(dict = {'a':1, 'b':2, 'c': 3}): for key in dict.keys(): print (key, dict[key]) ## range objects are implicit lists def it6(n = 6): R = range(n) it = iter(R) print (next(it)) print (next(it)) print (list(it)) ## enumerate objects are iterable def it7(text = "hello, world!"): it = iter(enumerate(text)) print (next(it)) print (next(it)) print (list(it)) ## can iterate through output of unix commands import os def it8(cmd = 'ls -l'): p = os.popen(cmd) for x in p: print (x, end='') ## map is an iterable def it9(lst = [1,2,3,4,5,6,7,8]): x = map(lambda x: x*x, lst) print (x) for e in x: print (e) def it10(lst = [1,2,3,4,5,6,7,8]): x = map(lambda x: x*x, lst) try: while x: print (next(x)) except: print ('done') # filter is an iterable def it11(lst = [1,2,3,4,5,6,7,8]): x = filter(lambda x: x%2, lst) print (x) for e in x: print (e) def it12(lst = [1,2,3,4,5,6,7,8]): x = filter(lambda x: x%2, lst) try: while x: print (next(x)) except: print ('done') ## zip is an iterable def it13(lst1 = [1,2,3,4], lst2=[5,6,7,8]): x = zip(lst1, lst2) print(x) for a,b in x: print (a,b) def it14(lst1 = [1,2,3,4], lst2=[5,6,7,8]): x = zip(lst1, lst2) print(x) try: while x: print (next(x)) except: print ('done') ## multiple iterators ## zip iterators are synchronized def it15(): z = zip([1,2,3], [4,5,6]) i1 = iter(z) i2 = iter(z) print (next(i1)) print (next(i1)) print (next(i2)) ## map iterators are synchronized def it16(): z = map(lambda x: x*x, [1,2,3]) i1 = iter(z) i2 = iter(z) print (next(i1)) print (next(i1)) print (next(i2)) ## range iterators are independent! def it17(): z = range(10) i1 = iter(z) i2 = iter(z) print (next(i1)) print (next(i1)) print (next(i2)) ''' What would it mean to have an iterator for the person class? To iterate over a person's friends and family? ''' ## iteration using deep recursion class Node: def __init__(self, value): self.value = value self.children = [] def __repr__(self): return 'Node({!r})'.format(self.value) def add_child(self, node): self.children.append(node) ## a depth first traversal of the tree def __iter__(self): print ("Calling from: " + str(self.value)) # first, yield everthing every one of the child nodes would yield. for child in self.children: for item in child: # the two for loops is because there's multiple children, # and we need to iterate over each one. yield item # finally, yield self yield self def it18(): root = Node(0) child1 = Node(1) child2 = Node(2) child3 = Node(3) child4 = Node(4) child5 = Node(5) child6 = Node(6) root.add_child(child1) root.add_child(child2) child1.add_child(child3) child3.add_child(child4) child4.add_child(child5) child5.add_child(child6) for ch in root: print (ch) # define an iterator using yield class yrange: def __init__(self, n): self.i = 0 self.n = n def __iter__(self): while self.i < self.n: i = self.i self.i += 1 yield i else: ## raise StopIteration() pass def it19(): print (list(yrange(5))) print (sum(yrange(5))) y = iter(yrange(3)) print (next(y)) print (next(y)) print (next(y)) # can use generators to create iterators def zrange(n): i = 0 while i < n: yield i i += 1 def it20(): print (list(zrange(5))) print (sum(zrange(5))) z = zrange(3) print (next(z)) print (next(z)) print (next(z)) # how yield and next operate together def foo(): print ("begin") for i in range(3): print ("before yield", i) yield i print ("after yield", i) print ("end") def it23(): f = foo() print (f) next(f) next(f) next(f) next(f) def integers(): """Infinite sequence of integers.""" i = 1 while True: yield i i = i + 1 def squares(): for i in integers(): yield i * i def take(n, seq): """Returns first n values from the given sequence.""" seq = iter(seq) result = [] try: for i in range(n): result.append(next(seq)) except StopIteration: pass return result ## added finally clause to return value even if exception is thrown. def take2(n, seq): """Returns first n values from the given sequence.""" seq = iter(seq) result = [] try: for i in range(n): result.append(next(seq)) except StopIteration: pass finally: return result def it24(): print (take(5,squares())) # can use comprehensions to create generators. Use () instead of [] g = (x*x for x in range(100)) def it25(): print (take(10,g)) noprimes = [j for i in range(2, 8) for j in range(i*2, 100, i)] yesprimes = (x for x in range(2, 100) if x not in noprimes) # does not work if noprimes is a generator def it26(): print (take(10,yesprimes))