Python - Control Flows - Looping

Featured image

This article is a part of the Python - 101 series, you can access the full version of the series here:

Welcome to the seventh article of the Python - Coding 101 series, which is also the starting of the Control Flows section. After reading this article, you’ll learn:

Remember that we have covered the basic of control flows in the first article of the series? In this article, we will dig deeper into the intuition behind looping, together with some techniques to write more efficient code. The outline of this post is as follow

1. Intuition behind looping

1.1. Iterables & iterators

We have been working with loops for some time now, we have known the syntax of a for loop, and some special characters that can be used like break, pass, etc. But do you know that loop is built on the basis of iterators and iterables.

In Python, iterable is an object that can be looped over. For instance: lists, dictionaries, strings, etc. All iterables can be passed to the built-in iter function to get an iterator from them.

>>> iter(['some', 'list'])
<list_iterator object at 0x7f227ad51128>
>>> iter({'some', 'set'})
<set_iterator object at 0x7f227ad32b40>
>>> iter('some string')
<str_iterator object at 0x7f227ad51240>

So what exactly is an iterator? You may ask. An iterator is an object representing a stream of data. It does the iterating over an iterable. You can use an iterator to get the next value or to loop over it. Once, you loop over an iterator, there are no more stream values, thus will result in an error if you want to access its next element.

>>> iterator = iter('hi')
>>> next(iterator)
'h'
>>> next(iterator)
'i'
>>> next(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

For and while loop all rely on iterables and iterators. You can understand that such loop is just the work of getting an iterator from an iterable and then repeatedly asking the iterator for the next item. So next time you look at a loop, remind that iterators are hiding behind the scenes.

Iterables and iterators are the core concept of lazy evaluation, which is a very helpful approach when working with big data.

1.2. Why do we need to know iterables & iterators

Iterators allow us to both work with and create lazy iterables that don’t do any work until we ask them for their next item. This is a very useful feature when working with a large amount of data. Because of their laziness, the iterators can help us to deal with infinitely long iterables. In some cases, we can’t even store all the information in the memory, so we can use an iterator which can give us the next item every time we ask it. Iterators can save us a lot of memory and CPU time.

This approach is called lazy evaluation.

2. List comprehension & Generators

2.1. List comprehension

2.1.1. What is list comprehension

Remember lambda function, with which we can declare a function in one line of code? With list comprehension, we can do the same with loop.

So list comprehension is just a better, more efficient version of for loop.

2.1.2. Why list comprehension

2.1.3. How to create list comprehension

Basic Syntax: [<output expression> for <iterator variable> in <iterable>]

Types of list comprehension:

2.2. Generators

2.2.1. What is generators

Generator is like a list comprehension, except it does not store the list in the memory. Generator does not actually construct the list, but it is an object that we can iterate over to produce elements of the list as required.

2.2.2. Why generators

2.2.3. How to create a generator

To create a generator, use the same syntax as if you were creating a list comprehension, but change the double brackets to parentheses: (<output expression> for <iterator variable> in <iterable>)

# original list
>>> nums = [2, 32, 12, 5]
  
# create a generator
>>> new_nums = (num + 1 for num in nums)

# check its type
>>> print(type(new_nums))
<class "generator">

# create a list from a generator
>>> print(list(new_nums))
[3, 33, 13, 6]

# or print it sequentially
>>> new_nums = (num + 1 for num in nums)
>>> for i in list(new_nums):
...   print(i)
3
33
13
6

You can also create a generator functions, which return a generator object when called. It is defined like a regular function (with def), but change the output expression to yield

# define a generator function
>>> def num_sequence(n):
...   """Generate values from 0 to n."""
...   i = 0
...   while i < n:
...     yield i
...     i += 1

# use it
>>> result = num_sequence(3)
>>> print(type(result))
<class "generator">

>>> for item in result:
...   print(item)
0
1
2