Map and Filter functions

Map and Filter functions

There's the last guy called reduce but it's not here because it's like the odd guy out and requires a dedicated article to it

Let us map

Before we map, let us take a look at a beautiful for-loop in Python.

squares = []

for i in range (2, 10):
    squares.append(i ** 2)

print(squares) # [4, 9, 16, 25, 36, 49, 64, 89]

The point of the above is to take the numbers 2 up to 10 (exclusive) and get the squares of them. With a map function, we can easily achieve that with grace and mercy.

The map function, included in the global namespace by default is an elegant way to perform operations on an iterable, basically replacing an expressive for-loop, like so:

# returns an iterator of our results
squares = map(lambda x: x ** 2, range(2, 10))

# we can then get a list with
squares = list(squares)

The map function accepts two arguments: a function and an iterable; and applies the function to each member of the iterable and spits the result as a generator. Generally, lambda functions are preferred as the function to be used due to its conciseness. We can with this define the internal implementation of a map function crudely to be:

from typing import (
  Any,
  Callable,
  Iterable,
  Generator
)


def map(func: Callable, iterable: Iterable) -> Generator[Any, Any, Any]: 
  # the first `Any` is the type of the value being returned
  # we can replace the `Generator` with an `Iterator`
  # but we're not here to learn about types
  for member in iterable:
    yield func(member)

# so we call it like so:
map(func, iterable)

With this information, map can (not every time) essentially replace your for-loops with a more concise definition that looks linear to the eye especially when you are performing simple operations. We can see more examples of map like so:

# This is only for the purpose of demonstration
names = list(map(
  lambda x: (input("Enter a name: ")).strip(),
  range(10)
))

# displays names
print(names)

Performance with the usual list wrapper

You may be concerned as to the performance implications of using a map and then wrapping the results in a list. Relieving your fears is the fact that map returns a generator-like (an iterator) result which ensures results are yielded only on demand. So wrapping the result in a list would have little or no performance overhead.

And then filter out

Filter on the other hand is a function with a similar structure with map except that it executes the function against the iterable and only returns value that evaluates to True. In this sense, it:

Filters out falsities and untrue things, revealing their true nature in an elegant display of absence.

A good way to use filter is a to filter out odd numbers from a list:

even_numbers = list(filter(
  lambda x: not (x % 2),
  range(100)
))

print(even_numbers) # -> 0, 2, 4, 6, 8, 10, ..., 98

The inner working of filter can be described crudely as:

from typing import (
  Any,
  Callable,
  Iterable,
  Generator,
  Optional
)

def filter(func: Optional[Callable], iterable: Iterable) -> Generator[Any, Any, Any]:
  for member in iterable:
    result = func(member)
    if result:
      yield result

# so we call it like so:
filter(func, iterable)

You'll notice that the func attribute is marked as optional. This is because you can pass None to filter, except that you'll get only values in the iterable that evaluate as True.

And so therefore

Go out there and populate codebases like JS spawns till you get the kind of comment I got on a PR:

A list comprehension is better than all these lambda and map you're putting everywhere.

If you're fascinated by lambda functions, those lambda x: x ** 2 you see in the filter and map functions, you can read my article on lambda functions.