Python (4/8) Code Flow


This is a part of the series of posts on Python Crash Course. Here, we take a step further into the language structures, to understand the code flow in Python.

Conditionals & Loops


Programming is all about data and decisions. In the above section, we saw a few ways data can be stored an processed in Python. Now, let us check out how decisions can be made, how code can be made to flow from one line to another. Then we will see a small code snippet to demonstrate basics of code flow.

Indentation


One major point where Python improves over most other languages is that it forces the developers to write readable code. It forces developers to indent the code; making it more readable. There are some developers who just have to write unreadable code - and they do find ways around Python as well.

Python does not use the curly braces {. . .} to define a block of code. A block of code is defined by its indentation. Consider the two blocks of code below:

a = 0
while a<10:
  a = a+1
  print(a)

a = 0
while a<10:
  a = a+1

print(a)

They show a typical while loop in Python. Note the ':' after the condition, followed by code that is indented. In the first block, both lines of code are indented, while the second block has print(a) outside the block. That made a big difference to the code flow. The print(a) when indented, was considered as a part of the while loop - hence executed 10 times. But, the same when not indented, is executed only once at the end of the loop.

This holds true for any block of code - if / elif / else / for / while / def (function) / class - anything that is appropriately indented is part of the block. Else, it is not.

Python does not insist on any specifics about space/tab indentation, number of spaces, etc. But the convention - that everyone follows - dictates that it should be 4 spaces.

I am not going to bore you (and myself) with the details of if / elif / else / while / for... We know them too well already. There are some subtle improvements in Python and we will see them as we go. Suffices here to say - Python provides for them. We will start with syntax of the basics and then move further.

I plan to just brush through the basics and jump to something more interesting. The code snippet below covers the basics of control flow. Check it out on your IDE!

Example


# Define an empty list
primes = []

# Define an empty set / map
divisors = {}

# Loop through all the numbers from 2 to 100.
for n in range(2, 100):
    divisors[n] = []
    # Loop through all values in list of primes
    for p in primes:
        # Break out of the loop if the number is
        # divisible by any of the primes
        if (not(n % p)):
            divisors[n].append(p)

    # This else block will be executed only if the
    # above for loop exits normally without a break
    # - implying that the number is prime.
    else:
        primes.append(n)

# Print the list of prime numbers and divisors
print(primes)
print(divisors)

Anyone who understands basic programming will surely understand what this code is doing. It just identifies the lists of divisors and prime numbers among the numbers 2-100.

The point to note here is the way code indentation defines the flow of the code. Most interesting is the else block. Because of its indentation, it is applied to the for loop rather than the if. Yes, Python also provides for an else block on a for loop - that is executed only if the loop reaches its natural end, without breaking off anywhere midway.

Iterators


In a previous sections, we saw loops on lists, tuples, etc.

for element in (1, 2, 3):
    print(element)

Python does not limit these iterations to its built-in collection types. Any class that implements the required methods can be used as an iterable. For example, you want to iterate over the first n elements of Fibonacci series, you can create a simple iterator for that:

class Fib:
    def __init__(self, n):
        self.max = n
        self.last = 0
        self.secondlast = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.last:
            self.secondlast, self.last = self.last, self.last + self.secondlast
        else:
            self.last = 1
        if self.last > self.max:
            raise StopIteration
        return self.last

for x in Fib(100):
    print(x)

In fact, you need not implement the next method in the object that you are working with. All you need is the iter() method. This method should return an object which implements the next()

def fib(n):
    i, j = 0, 1
    while j <= n:
        yield(j)
        i, j = j, i+j

    for x in Fib(100):
        print(x)

This is all you need! Note that keyword 'yield'. It is not return. The method does not return anything. It yields one value at a time. Of course, internally this translates to an iterator of some sort. But, the code certainly looks much better and much easier to manage. Since we have lesser script, it means there is lesser effort on interpretation and more of the C code doing the job. Naturally the performance is better.