#!/usr/bin/python3 # -*- coding: utf-8 -*- """ Iterators Python offers a fundamental abstraction called the Iterable. An Iterable is an object that can be treated as a sequence, for example being looped over, cast into another container type, mapped or joined. For example, the object returned by the range function is an iterable. Considering the back-end, an iterable is an object which implements the iterator protocol, which consist of the methods __iter__() and __next__(). """ # https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class from __future__ import annotations from typing import Sequence, Any, Iterator print(range(5)) print(type(range(5))) print(list(range(5))) # An example: filled_dict = {"one": 1, "two": 2, "three": 3} our_iterable = filled_dict.keys() print(our_iterable) print(list(our_iterable)) # => dict_keys(['one', 'two', 'three']). # This is an object that implements our Iterable interface. # We can loop over it. for i in our_iterable: print(i) # Prints one, two, three # However we cannot address elements by index. try: our_iterable[1] # raises a TypeError") except Exception as e: print(type(e), e) # An iterable is an object that knows how to create an iterator. # iterator is an object that can remember the state # as we traverse through it. # Generally, we get the next object with "next()". try: next(our_iterable) # Not possible yet, because not an iterator yet except Exception as e: print(type(e), e) print(type(our_iterable)) our_iterator = iter(our_iterable) print(type(our_iterator)) print(next(our_iterator)) # => "one" # It maintains state as we iterate. print(next(our_iterator)) # => "two" print(next(our_iterator)) # => "three" # After an iterator has returned all of its data, # it raises a StopIteration exception try: print(next(our_iterator)) # Raises StopIteration except Exception as e: print(type(e), e) # For another example, we re-create the iterator # We can also loop over it, in fact, "for" does this implicitly! our_iterator = iter(our_iterable) for i in our_iterator: print(i) # Prints one, two, three # You can grab all the elements of an iterable or iterator # by calling list() on it. print(list(our_iterable)) # => Returns ["one", "two", "three"] our_iterator = iter(our_iterable) print(list(our_iterator)) # => Returns [] because iteration state is "saved" # The built-in function iter takes an iterable object, and returns an iterator new_iterator = iter([1, 2, 3]) print(new_iterator) print(next(new_iterator)) for item in new_iterator: print(item) # These have been using iterators all along! for element in [1, 2, 3]: print(element) for element in (1, 2, 3): print(element) for key, value in {"one": 1, "two": 2}.items(): print(key, value) for char in "123": print(char) for line in open("foo.txt"): print(line, end="") """ What is really is going on when a for loop is executed? The function 'iter' is applied to the object following the 'in' keyword An iterator can be created from an iterable by using the function 'iter'. To make this possible the class of an object needs either a method '__iter__', which returns an iterator, and the __next__ function, or a '__getitem__' method with sequential indexes starting with 0, and potentiall __len__ (older). """ s = "abc" it = iter(s) print(next(it)) print(next(it)) print(next(it)) it = iter(s) for char in it: print(char) # Making an iterable object class MyIntegers: def __init__(self) -> None: print("hey") def __iter__(self) -> Iterator[int]: print("in here") self.a = 1 return self def __next__(self) -> int: x = self.a self.a += 1 return x # Step this myobject = MyIntegers() print(type(myobject)) # You have to call iter IF you init within it # Step this myiterable = iter(myobject) print(type(myiterable)) # Step this: print(next(myiterable)) print(next(myiterable)) print(next(myiterable)) print(next(myiterable)) print(next(myiterable)) # What happens if we for-loop through myiterable?? # To prevent the iteration from going on forever, # use the StopIteration statement. class Reverse: """Iterator for looping over a sequence backwards.""" def __init__(self, data: Sequence[Any]) -> None: self.data = data self.index = len(data) def __iter__(self) -> Iterator[Any]: return self def __next__(self) -> Any: if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index] # Step this rev = Reverse("spam") # We init in __init__ instead of __iter__ so don't need to call iter to call next: print(next(rev)) # Step this. iter(rev) is call, because rev may not be an iterable yet (it is this time). for char in rev: print(char) # Let'n re-invent something like range: class MyRange: """ The __iter__ method can make a non-iterable an iterator. Behind the scenes, the iter function calls __iter__ method on the given object. The return value of __iter__ is an iterator. It should have a __next__ method and optionally raise StopIteration, when there are no more elements. """ def __init__(self, n: int) -> None: self.i = 0 self.n = n def __iter__(self) -> Iterator[int]: return self def __next__(self) -> int: if self.i < self.n: self.i += 1 return self.i else: raise StopIteration() # Constructor calls __init__ nums = MyRange(5) # next calls __next__ # while we can do this without __iter__ , # without iter we can't use a for loop (which calls __iter__) print(next(nums)) # iter calls __iter__ num_iter = iter(nums) # nums and num_iter are the same object (all by reference) print(next(num_iter)) for num in nums: print(num) try: print(next(num_iter)) # StopIteration except Exception as e: print(type(e), e) # alternative (step this) for item in MyRange(5): print(item) # +++++++++ Cahoot-20.1 # https://mst.instructure.com/courses/58101/quizzes/57263