Iterators
Generators
Generators were introduced in Python a long time ago (PEP-255), with the idea of introducing iteration in Python while improving the performance of the program (by using less memory) at the same time. The idea of a generator is to create an object that is iterable and, while it's being iterated, will produce the elements it contains, one at a time. The main use of generators is to save memory—instead of having a very large list of elements in memory, holding everything at once, we have an object that knows how to produce each particular element, one at a time, as it is required. This feature enables lazy computations of heavyweight objects in memory, in a similar manner to what other functional programming languages (Haskell, for instance) provide. It would even be possible to work with infinite sequences because the lazy nature of generators enables such an option.
next()
The next()
built-in function will advance the iterable to its next element and return it.
itertools
This is not only non-Pythonic, but it's also rigid (and rigidity is a trait that denotes bad code). It doesn't handle changes very well. What if the number changes now? Do we pass it by parameter? What if we need more than one? What if the condition is different (less than, for instance)? Do we pass a lambda? These questions should not be answered by this object, whose sole responsibility is to compute a set of well-defined metrics over a stream of purchases represented as numbers. And, of course, the answer is no. It would be a huge mistake to make such a change (once again, clean code is flexible, and we don't want to make it rigid by coupling this object to external factors). These requirements will have to be addressed elsewhere.
itertools.islice
- Takes First Ten
There is no memory penalization for filtering this way because since they are all generators, the evaluation is always lazy. This gives us the power of thinking as if we had filtered the entire set at once and then passed it to the object, but without actually fitting everything in memory. Keep in mind the trade-off mentioned at the beginning of the chapter, between memory and CPU usage. While the code might use less memory, it could take up more CPU time, but most of the times, this is acceptable when we have to process lots of objects in memory while keeping the code maintainable.
Repeated Iterations with itertools.tee
In this example, itertools.tee
will split the original iterable into three new ones. We will use each of these for the different kinds of iterations that we require, without needing to repeat three different loops over purchases.
Yielding
Iterator but Not Iterable
Sequence are Iterables
Coroutines
.close()
.throw()
.send()
Python takes advantage of generators in order to create coroutines. Because generators can naturally suspend, they're a convenient starting point. But generators weren't enough as they were originally thought to be, so these methods were added. This is because typically, it's not enough to just be able to suspend some part of the code; you'd also want to communicate with it (pass data and signal changes in the context).
close()
When calling this method, the generator will receive the GeneratorExit
exception. If it's not handled, then the generator will finish without producing any more values, and its iteration will stop.
throw()
This method will throw the exception at the line where the generator is currently suspended. If the generator handles the exception that was sent, the code in that particular except
clause will be called; otherwise, the exception will propagate to the caller.
send(value)
First None
yield from
yield from iterable
Async Programming
Async Context Managers
await async_iterator.__anext__()