Python

# 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. 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: 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: 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: 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"],
["Maria", "Pereira"]]
print(f"{'First Name':10} {'Last Name':10}")
for name in names:
print(f"{name:10} {name:10}")
```

Output: 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: ## 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: 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
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: 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: 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: 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: 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: 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: 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: 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: ## 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: ## 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: 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: 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: 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}")
``` 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. ## 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.