python enumerate iterator flow mechanism
Aug. 31, 2019 * Python Programming

Python: Enumerate counter for loops over list, tuple, string

The Python interpreter has a small set of core built-in functions. Enumerate simplifies code by adding a parallel counter to an iterable object like a list, tuple or string. Implementation of an enumerate object is memory efficient and lazy, which we will discuss.

Syntax:
enumerate( iterable, start=0 )

iterable: sequences like string, list, tuple or generator functions
start: counter initial value by default is zero, but can be set to any integer

Counters, loops and indexed elements

Counters and loops represent well-known methods for accessing items within an array or list. Consider we have a list of symbols for first ten elements in the periodic table. We want to print the atomic numbers and the symbols side by side. So Hydrogen should show up as '1 H', and Neon as '10 Ne'. Let us do this the conventional way, which needs a loop with a counter to access the elements within the list in sequence.

Conventional loops and counters

periodic = ['H','He','Li','Be','B','C','N','O','F','Ne']

for element in range(0, len(periodic)):
  atomic = element + 1
  print(atomic, periodic[element])
Code output:
1 H
2 He
3 Li
4 Be
5 B
6 C
7 N
8 O
9 F
10 Ne

The code satisfies the original intent. We have a loop to count from 0 to 9, to get access to the sequence of symbols in the list. However since atomic number starts at 1, we have to get another variable to adjust the values before we print out the pair.

Next, let us do it the Pythonic way.

Iterating with enumerate

The list of symbols represents an iterable object which we pass on to enumerate. We also change the default starting value from 0 to 1. We start iterating by requesting a pair of values from enumerate. The iteration ends when we reach 'Ne'.

Iterating with enumerate

periodic = ['H','He','Li','Be','B','C','N','O','F','Ne']

for atomic, element in enumerate(periodic, 1):
  print(atomic, element)
Code output:
1 H
2 He
3 Li
4 Be
5 B
6 C
7 N
8 O
9 F
10 Ne

The result is the same as before. The code is a completely different story. We have created a new iterator with enumerate, one that provides a pair of values. The first is like a counter representing the atomic number, which we have set to start at '1' instead of '0' which is the default value. The second value is the symbol for the element. We just loop through requesting a pair of values to be printed - the rest is done by the Python iterator.

Why is this better?

Doing it the Pythonic way is clean, concise and faster. It also provides less headache in terms of book-keeping. Let us dig a little deeper.

Iterator object in Python

Instead of iterating with the enumerate iterator to start with, let us just create the iterator and store it as an object with a name.

Book-keeping using iterators

periodic = ['H','He','Li','Be','B','C','N','O','F','Ne']

#iterator object
elements = enumerate(periodic, 1)
print(elements)

#request 2 elements
print('First row of periodic table')
print(next(elements))
print(next(elements))

#request 8 elements
print('Second row of periodic table')
for row1 in range(0,8):
  print(next(elements))
Code output:
<enumerate object at 0x00000213DDB8AF30>

First row of periodic table
(1, 'H')
(2, 'He')

Second row of periodic table
(3, 'Li')
(4, 'Be')
(5, 'B')
(6, 'C')
(7, 'N')
(8, 'O')
(9, 'F')
(10, 'Ne')

This time we create a stand-alone iterator object with enumerate and name it as elements. After initialization, we request single elements two times with the next function. The iterator sends us (1, 'H') and (2, 'He') which represents the first row of the periodic table. Note that value returned by elements is a tuple containing an integer and a string, which represents the atomic number counter, and the symbol respectively.

Let us take a break from iteration, and state that the second row of the periodic table is to be printed. There are 8 elements left, and so we create a loop keep calling the iterator to send us the next element till we reach the end.

Was the difference noted?

We would not have expected to start a counter loop, and in the middle of looping go and do something else. Once we return and start looping again, we find ourselves just where we left off. The overhead and complication to manage this small task conventionally would be huge!. Here in Python we have the iterator object, which once initialized is mindful of its state, so that the programmer can do something more useful.

Generators, custom iterator functions

Not everything about enumerate is magical. For one, we can create our own custom iterator function, also known as a generator. Here is an example to count odd numbers. It has limitations and will throw errors if taxed beyond its limit - but it should demonstrate how things work under the hood.

We have a list of odd numbers from three to nine as text. The idea is to get the numeric values side by side. Enumerate all by itself will not work, as even if we start it off at 3, it will return 4 next when it should send back 5.

Custom iterator function

#custom iterator function
def oddnumbers( iterable, start ):
  counter = start
  for item in iterable:
    yield item, counter
    counter += 2

#iterable list
oddlist = ['three','five','seven','nine']

#iterator object
numbers = oddnumbers(oddlist, 3)

print(next(numbers))
print(next(numbers))
print(next(numbers))
print(next(numbers))
Code output:
('three', 3)
('five', 5)
('seven', 7)
('nine', 9)

A custom iterator solved the problem. We created a function that 'yields' one set of values when called by 'next' function. Internally it loops through the list of iterables, which in this case were the odd numbers in text. The internal counter was externally set to 3 during initialization to match the first number in the list. From there, the iterator needed to increment by 2 to ensure the next number to return would be odd, and match the sequence. The order of elements in the returned tuple was also reversed to demonstrate that counter need not be the first item.

References