Looping...
It is every programmer's favorite activity.
Who does not love doing the same thing over and over and over ... and over again.
Cyberspace is a very loopy place.
To make the act of iteration a pleasant one,
Python gives you many ways to easily - and efficiently - loop over things.
Today we will discuss iterables and the itertools module to see how for-loops work.
Along the way, we will showcase many additional tools available in Python.
So let us now initiate - a lesson that will educate -
and even help elucidate - all the ways to iterate.
In Python, if you can loop over something in a for-loop,
... then we call that something an "iterable."
The process of looping over the elements is called "iterating."
You can loop over many different objects in Python.
For example, if you have a list...
...then you can loop over each element in that list.
A list is one type of a sequence.
A sequence is an iterable with a clear order to the components.
Other common sequences are tuples, strings, and bytes.
Here we loop over the values in a tuple...
You can also iterate over the letters in a word, like 'Socratica'...
And for a "bytes" object, you can loop over the contents one byte at a time.
The byte values are the ASCII codes for each letter.
You cannot iterate over the digits of an integer.
To quote the error message: integers are NOT iterable objects.
So if you need to loop over the digits of an integer
and have the digits treated as integers,
you will need to construct this yourself.
We can extract the digits by iterating over the characters in the string version of the number,
and converting each character to an integer.
Now we can loop over the digits.
This example raises an interesting question.
What makes an object "iterable?"
To understand the answer, you need to know your ABCs.
Starting in Python version 3.3,
there is a collections.abc module.
A-B-C is short for "Abstract Base Classes."
Today, we are interested in these rows.
What will we be interested in tomorrow? Who knows...
From these rows, we see that the key to iteration lies in two special methods: __iter__ and __next__
The __iter__ special method returns an iterator object.
The __next__ special method returns the next item from the collection.
Repeated calls to __next__ will go through the collection one item at a time.
When there is nothing left to iterate over, a StopIteration exception is raised.
To fully appreciate the kindness of Python,
we will imitate a for-loop by calling the __iter__ and __next__ methods ourselves.
We start by creating a tuple of strings.
Instead of using a for-loop, we will access the iterator directly.
One way to do this is to call the __iter__ special method.
By looking at the type, we see this gives us an iterator.
Now we can step through the tuple by calling the __next__ special method.
Once...
Twice...
Thrice...
And brace yourself for the fatal fourth call.
I knew that was going to happen, yet I still flinched.
Python has builtin functions to spare you from typing underscores.
You can use the "iter" function to get the iterator.
And then you can pass the iterator to the "next" function.
First...
Second...
Third...
Loopus interruptus...
Now that we have seen the "iter" and "next" functions,
let us use them to replicate a for-loop.
We create a short list of usernames for this demonstration.
To see our objective, let us perform a basic loop.
Run...
To do this without using the for-loop, we need to first get an iterator.
We initiate an infinite loop with a "while True" command.
We only stop calling "next" once a StopIteration exception is thrown.
We use a "try-except" because calling the "next" function can raise an exception.
If an exception is not thrown, then our "next" call was successful.
So now we can print the user name.
But we also need to handle the exception that signals our looping days are over.
Run...
These 7 lines should help you understand the mechanics of iteration.
While these 2 lines should help you appreciate the for-loop.
Now that we understand the mechanics of iteration, let us create a class with iteration built in.
Our class will be called "Portfolio."
The constructor will simply create a dictionary to hold the number of shares in each asset.
In finance terminology, the shares in a portfolio are called the "holdings."
We will add a buy method that will increase the holdings in 'ticker'
by the specified number of shares.
If this is the first time purchasing this asset,
we will use a default value of zero shares.
Next, we add a 'sell' method.
We could technically use a single method to handle buys and sells.
A positive number of shares would constitute a buy,
while a negative number of shares would be a sell.
But I have already typed these methods, so there is nothing to be done.
We would like the ability to iterate over the holdings in a portfolio.
To do this, we need to implement the __iter__ special method.
All we need to do is return an iterator.
And the key-value items in a dictionary are already iterable.
So we simply return that iterator.
Now we can create a portfolio.
Let us invest in some imaginary companies.
And then loop over the portfolio and display the holdings.
Run...
Our class is now iterable.
Iterating is such a common task that Python has a module devoted to it.
The "itertools" module contains a variety of functions for iteration.
These functions are grouped into 3 categories.
The first are "infinite iterators."
If you do a for-loop using one of these functions, the loop will continue forever.
It is up to you to stop the loop once a goal has been achieved.
The next group of functions gives you the ability...
to perform common pre-processing on the collection of things to loop over.
And the final group of functions makes it easy to do calculations...
... involving combinations and permutations from a set.
To demonstrate a few of the iterators in the "itertools" module,
we will do a little card counting.
More specifically, we will construct a list of all possible hands in poker.
First, import the "itertools" module.
Playing cards are determined by their rank and their suit.
The possible ranks are the numbers 2 through 10,
along with Jacks, Queens, Kings, and Aces.
Remember, the "range" function does not include the final number,
because that would make sense.
If you print this list, you will see it contains a mixture of integers and strings.
This fills me with a sensation that is 2 parts frustration and 1 part shock,
so I will make every rank a string.
Run...
There are four suits: hearts, clubs, diamonds, and spades.
The deck of 52 cards is the product of the ranks and the suits.
Thanks to the "product" iterator, we can build this list with a single list comprehension.
To dispel any worries, we will demonstrate that all the cards are there.
Run...
There is nothing up my sleeve.
Next, we can create a list of all possible 5 card poker hands by using the "combinations" iterator.
Rather than loop over every possible hand, we will simply print the number of hands.
Run...
Nearly 2.6 million possible hands.
Now that we have iterated over many iterables,
I wanted to "loop back" and say a few closing remarks.
*clears throat*
A few closing remarks...