Python map() Function (Loop without a loop)

Have you been using for loops to perform repetitive tasks on a list of items in Python?
Did you wish there existed a more efficient way to apply functions to every item in a Python list?
If you answered yes, you are yet to discover an important and powerful tool in Python – the map() function.

In this tutorial, we will uncover the capabilities of a map function that helps us not only implement more efficient iterations than for loops but also helps us write clean code.

 

 

What does the map function do?

The map function helps us iteratively apply a function to all the items in a Python list, or in any Python iterable for that matter, with just one line of code.
The map function accepts two parameters, the first of which is the function to be applied to the individual items in an iterable (list, tuple, set, etc.), and the second parameter is iterable itself.
The map function returns a map object, which can be converted into a desired iterable (list, tuple, set, etc.) by calling the appropriate method.

map function demonstration

Let us consider a Python function to convert a temperature in Fahrenheit to its Celcius equivalent.
We will apply this function to a list of temperatures collected from, say different cities.

def fahrenheit_to_celcius(F):
    C = (F-32)*(5/9)
    return round(C,4)
temp_fahrenheit = [100, 95, 98, 105, 110, 32]
temp_celcius =  []
for tf in temp_fahrenheit:
    tc = fahrenheit_to_celcius(tf)
    temp_celcius.append(tc)
print(f"Temperatures in Fahrenheit: {temp_fahrenheit}")
print(f"Temperatures converted to Celcius: {temp_celcius}")

Output:

using for loop to iterate a list and apply function

Here, we have taken a traditional approach of iterating over a list i.e using for loop.
We first created an empty list temp_celcius and then inside the for loop, we are accessing every item in the list temp_fahrenheit.
We call the method fahrenheit_to_celcius on these items and append the result to temp_celcius.

Let us see how both these steps can be replaced by a single call to the map function.

temp_celcius_map = list(map(fahrenheit_to_celcius, temp_fahrenheit))
print(f"Temperatures converted using map: {temp_celcius}")

Output:

using map to iteratively apply function to items in a list

Notice how we have eliminated the for loop and initialization of the empty list and replaced them both with a single call to the map function.
We are using the method list to convert the returned map object into a list. We could have equivalently used the tuple() method if we wanted our result to be a tuple.

 

map function over tuple

As discussed earlier, the map method can be called on any valid Python iterable, such as tuple, string, dictionaries, etc.
Let us take an example of its usage on a tuple.

Here we will use the map function to apply the str.lower method to a bunch of names stored as strings in a tuple.

names = ("John", "Adam", "STANLEY", "toNy", "Alisha")
print(f"names: {names}")
names_lower = list(map(str.lower, names))
print(f"names in lowercase: {names_lower}")

Output:

calling map over a tuple

Here we did not pass a user-defined function like earlier. We instead passed an in-built method of the string module in Python.

Note we must pass the function name (without parentheses) and not the function call, as the first parameter to map.

 

map over nested lists

So far, the individual values in the iterables on which function was being called, were single scalar values.
We can also apply the map function to nested lists. Here, the function we pass to map will accept a list(or tuple) as its parameter.

Let us consider a list of names of individuals. These names will not be stored as single strings.
They will instead be defined as a list of 2 strings, the first of which will store the first name, and the second element in the list will be the last name.

names = [["Stephen", "Hawking"],
         ["John", "Doe"],
         ["Christian", "Wolf"],
         ["Aditi", "Singh"],
         ["Maria", "Pereira"]]
print(f"{'First Name':10} {'Last Name':10}")
for name in names:
    print(f"{name[0]:10} {name[1]:10}")

Output:

Defining a nested list of first and last names

We will define a function that accepts a list carrying the first name and the last name, and returns a single concatenated string representing the full name of the individual.
We will use map to apply this function to all the names in the list we defined above.

def get_full_name(name):
    return " ".join(name)
full_names = list(map(get_full_name, names))
print(f"full names: {full_names}")

Output:

Joining first & last names using map

 

map on dictionary

We have seen the usage of map on lists, and tuples. Let us now understand how we can leverage the function to process dictionaries.
Iterating over dictionaries is not as straightforward as it is with lists, tuple, etc., because dictionaries store a collection of key-value pairs.
If you use a for loop to iterate over a dictionary variable, the iterator variable will be assigned a key of the dictionary during each iteration.

Let us understand this by defining a dictionary electricity_bills whose keys are consumer ids of customers of a power company, and values are tuples carrying the consumer name and a list of electricity bill amounts of the last 6 months.

 electricity_bills = {
     11001: ("Pete Wolfram",[100, 85, 200, 150, 96, 103]),
     11002: ("Jessica Becker", [76, 88, 102, 97, 68, 72]),
     11003: ("Alex Jasper",[170, 190, 165, 210, 195, 220]),
     11004: ("Irina Ringer",[350, 284, 488, 372, 403, 410]),
     11005: ("Sean Adler",[120, 115, 111, 109, 121, 113])
 }
for k in electricity_bills:
    print(k)

Output:

Using for loop to iterate a dictionary

We can only access the keys of the dictionary if we iterate over it directly. Similar behavior will be exhibited by the map function.
The function that we pass to map, will be iteratively called with only the keys of the dictionaries.
But here we want to process the values of the dictionary as well. And so the function we pass to the map function should receive both the keys and values in the dictionary.
We can achieve this by calling the items method on the dictionary and using the same as the iterable for the map function instead of using the dictionary directly.
The items method returns a dict_items object which has the key-value pairs of the dictionary as tuples in a list.

Let’s define a function that accepts such key-value pairs, computes the average monthly bill of the customer, and returns a tuple having the consumer id and the monthly average bill.
We will then use this function with map to find the average bills of all the customers in the dictionary.

def calculate_average_bill(consumer):
    # consumer is a tuple having key-value pair
    key, value = consumer
    consumer_id = key
    bill_amounts = value[1]
    avg_bill = sum(bill_amounts)/len(bill_amounts)
    return(consumer_id, round(avg_bill,4))
average_bills = list(map(calculate_average_bill, electricity_bills.items()))
print(f"average monthly bills: {average_bills}")

Output:

Using map with dictionary items

Thus we got a list of tuples, each having consumer_id and the average monthly bill.

We can similarly call the values() function on the dictionary if we want to process just their values.

 

map with multiple iterators

So far, We have been passing a single iterable to the map function, along with a function that expects a single argument so that the individual items from that iterable are passed to the successive function calls.

We can also define a function with multiple arguments, each of which can come from a separate iterable.
Let us define a function that accepts two numbers and returns their GCD or ‘Greatest Common Divisor’.

def gcd(a,b):
    if a < b:
        a,b = b,a
    while(b!=0):
        a, b = b, a%b
    return a
print(f"gcd of 45 and 30 is {gcd(45,30)}")

Output:

a function that calculate gcd of two numbers

We will define two separate lists of equal lengths and pass them to the map function along with the method to calculate gcd.

The map function will iteratively call the method gcd(a,b) whose first parameter will be taken from the first list, and the second parameter will be taken from the second list.

x = [45, 3, 18, 27, 37]
y = [30, 5, 12, 81, 9]
print(f"x: {x}")
print(f"y: {y}")
gcds = map(gcd, x, y) # calling map with more than 1 iterables
gcds = list(gcds)
print(f"GCDs: {gcds}")

Output:

calling map with 2 lists

Note that the two lists have to be of the same length because the parameters are passed to the function pairwise, one each from the two lists.
If the two lists are not of the same length, then the minimum possible pairs will be processed and the extra elements in the longer list will be discarded. The result in such a case would be of length matching the length of the smaller list.

Note that the two (or more) iterables we pass to map do not necessarily have to be of the same type.
That means if one iterable is a list, the other one could be a tuple, and the third one could be a set, and so on.

Let us define a function that accepts 3 values – name of a student (string), their roll no. (int) and cgpa (float), and returns a dictionary having each of these items labeled by their keys.
We will pass this function to the map function along with 3 iterables.

import numpy as np
def get_student_dict(name, roll, cgpa):
    student = {
        "name": name,
        "roll no.": roll,
        "CGPA": cgpa
    }
    return student   
names = ["Adam", "Becka", "Brandon", "Charlotte", "Mariyam"] # a list(length=5)
roll_nos = (1, 2, 3, 4, 5) # a tuple(length=5)
cgpa = np.array([9.2, 7.6, 8.5, 9.8, 8.7, 4.8]) # a NumPy array(length=6)
print(f"names = {names}, type={type(names)}\n")
print(f"roll_nos = {roll_nos}, type={type(roll_nos)}\n")
print(f"cgpa = {cgpa}, type={type(cgpa)}\n")
student_dicts = map(get_student_dict, names, roll_nos, cgpa)
print("Student dictionaries:\n")
for student in student_dicts:
    print(f"{student}\n")

Output:

calling map with 3 different iterables

There are a couple of things to note here:

  1. The three iterables we passed to map are each of a different type – a list, a tuple, and a NumPy array.
  2. These iterables are not of equal length, the cgpa array has an extra value which is discarded by map.
  3. We are not converting the returned map object into a list or a tuple. Since it is an iterable, we are directly iterating on it using the for loop.

 

map with lambda

Until now, We have been defining the function to be passed in advance before calling the map function.
But Python map function’s true potential is realized when it is used with lambda functions.

Let us first understand what lambda is.

lambda is a Python keyword that is used to create anonymous functions.
An anonymous function, as the name suggests, is a function with no name.

Our typical way of defining a function, using the def keyword, involves declaring the function with a name. We need to define such a function only once, and we can use it as many times as we want, at different places in our program.
An anonymous function, on the other hand, is constructed without a name and is generally not meant to be reused at multiple positions.

The syntax for lambda function declaration is: lambda arg1, arg2,... : expression
A lambda function can accept more than 1 argument, but its return value has to be an expression. That means, it cannot have multiple Python statements like a normal function before returning a value.

Let us define a lambda function to find the square of a value.

square = lambda x: x**2
print(f"Square of 12 = {square(12)}")

Output:

lambda function basic usage

Note that a lambda function doesn’t have any explicit return statement, the ‘expression’ that we specify is evaluated and returned by the function.
Also note that though we have assigned the lambda function to a variable called ‘square’, it is not necessary to do so and is done here only for convenience.
We can very well define a lambda function and call it at the same time, without assigning it to any variable.

x = (lambda x: x**2)(25) #creating and calling lambda in single step
print(f"square of 25 = {x}")

Output:

calling lambda without a name

Lambda functions are particularly useful where we have to pass function objects as parameters to other functions, like in the case of map.

Let us now call map with a lambda function to calculate square roots of all the numbers in a list.

a = [144, 25, 400, 81, 36]
print(f"a = {a}")
square_roots = map(lambda x: x**(0.5), a) #using lambda to compute square roots
square_roots = list(square_roots)
print(f"square roots = {square_roots}")

Output:

using lambda with map to compute square root

Let us also take an example of a lambda with multiple arguments.
We’ll define a lambda that accepts two arguments and returns their product.
We will then use this in a map function with two lists, to find pairwise products of the values in the two lists.

a = [1, 2, 3, 4, 5]
b = [10, 20, 30, 40, 50]
print(f"a = {a}")
print(f"b = {b}")
products = list(map(lambda x,y: x*y, a, b))
print(f"products = {products}")

Output:

using lambda with multiple arguments on map with multiple lists

 

map with conditions in lambda

In the previous section we discussed that we cannot use normal Python statements in a lambda function, and we have to represent the return value as an expression.
If we have to use if..else construct, however, we can include them as part of the expression using the following syntax:
lambda args: val1 if condition else val2

Let us understand this by defining a lambda function to find if a given value is even or not. We can use it on a list of numbers using map.
The result will be a list of boolean values indicating whether the corresponding values in the list are even or not.

a = [13, 60, 0, 2, 17, 19]
print(f"a = {a}\n")
is_even = list(map(lambda x: True if x%2==0 else False, a))
print(f"is_even(a) = {is_even}")

Output:

using lambda with if-else condition in a map function

 

map vs list comprehension

We have been using map to construct a list by applying a function to individual elements of a list.
There exists an alternative way in Python to construct such lists called. It is called list comprehension.

Before comparing map with list comprehension, let us first understand what is list comprehension.

List comprehension is a shorter, more elegant way of constructing a list than a for loop.
With list comprehension, you can construct a list on the go wherever you need it without having to initialize an empty list and then appending values to it.

Let us construct a list of squares of 1st 10 numbers using list comprehension.

squares = [x**2 for x in range(10)]
print(squares)

Output:

basic use of list comprehension

Let us now use list comprehension to replace the map function in our first code example of this tutorial.

def fahrenheit_to_celcius(F):
    C = (F-32)*(5/9)
    return round(C,4)
temp_fahrenheit = [100, 95, 98, 105, 110, 32]
temp_celcius =  [fahrenheit_to_celcius(x) for x in temp_fahrenheit] #list comprehension
print(f"Temperatures in Fahrenheit: {temp_fahrenheit}")
print(f"Temperatures converted to Celcius: {temp_celcius}")

Output:

replacing map by list comprehension

In this example, defining the function fahrenheit_to_celcius was still the part common with using map.
However, if we consider map with lambda functions, we can get rid of lambda as well using list comprehension.
We can use the same expression that lambda uses to construct our list.

Let us redo the code for finding ‘is_even’ on a list of numbers, using list comprehension.

a = [13, 60, 0, 2, 17, 19]
print(f"a = {a}\n")
is_even = [True if x%2==0 else False for x in a]
print(f"is_even(a) = {is_even}")

Output:

replacing map and lambda together by list comprehension

Thus we were able to do away with both map and lambda with a single list comprehension.

As far as the comparison between the two is concerned, list comprehension comes out as a more clean and syntactically more intuitive option than map.
It is also considered a more ‘Pythonic’ way of doing things than map.

Another advantage of using list comprehension is that we can use it to filter items from a given list based on some conditions
and construct a list of smaller lengths than the original list.
This is not the case with map where, for every item in the input list, an item has to be returned by the function passed.

Let us look at an example where we use list comprehension to filter out multiples of 5 from a given range of numbers.

multiples_of_5 = [x for x in range(1,31) if x%5==0]
print(f"Multiples of 5 from 1 to 30 = {multiples_of_5}")

using list comprehension to filter items

This is not possible with map.

If we consider the time comparison between the two methods, map performs a faster execution than list comprehension.
The following image shows time logs of the two approaches run in a Jupyter notebook.

time comparison between list comprehension and map

 

Conclusion

In this tutorial, we got introduced to the map function, a powerful utility for iterative processing of lists, tuples, etc.
We first understood what a map function does by comparing it with a for loop.
Then, we saw the usage of map function on various data structures such as lists, tuples, etc. We used the map function over nested lists as well.
We also used map function on dictionaries to iterate over their keys, values, and key-value pairs.

Then, we used map with multiple iterables and a function with multiple arguments.
We introduced Python lambda and understood the concept of anonymous functions. We learned the usage of Python lambda with map functions.
We also saw how we can use if conditions in lambda, and used one such lambda function with map.

Finally, we compared map with list comprehension which is an alternative approach to constructing lists from an existing iterable.

Leave a Reply

Your email address will not be published. Required fields are marked *