Functional programming in Python

Basic Concepts

Basic Concepts

Functional programming in Python is a programming paradigm that treats functions as first-class citizens and emphasizes immutability, pure functions, and higher-order functions.
Key Concepts:
First-Class Functions : Functions can be assigned to variables, passed as arguments, and returned from other functions.
Pure Functions : Functions that always return the same output for the same input and have no side effects. I.e. no additions to the primary purpose of the function - to compute and return a value.
Higher-Order Functions : Functions that take other functions as arguments or return functions ().
Immutability : Avoiding changes to data structures and preferring immutable data.
Lambda Functions : Anonymous functions for short operations (lambda x: x * 2).
, map(), filter(), and reduce() are examples of functional programming in Python.

The map() Function

The map() Function

./images/map_function.png

Syntax

map() The map() function in Python is a tool for transforming elements within an iterable.

                map(function, iterable, ...)
            
function - a function definition.
iterable - a sequence (list, tupple, range or string) or any iterable object. Note, that you can pass multiple iterables!
map() applies the function to each element of the iterable and returns a new iterator containing the transformed elements.

Example: map list of numbers to list of squared numbers

                numbers = [1,2,3,4,5]

                sqr_numbers = map(lambda x:x**2, numbers)

                print( list(sqr_numbers) )

                #[1, 4, 9, 16, 25]
            

Examples

Example: Generate all uppercase cyrillic letters by their UTF codes

                letters = map(chr, range(1040, 1072))
                print( list(letters) )

                #['А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я']
            
Above example can be done with list comprehenstions, as well:

                letters = [chr(code) for code in range(1040, 1072)]
                print( list(letter

                #['А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я']
            
Example: feed map() with multiple iterables. Note that this example can not be done easilly with list comprehensions

                l1 = [1,2,3]
                l2 = [1,2,3]

                l_sum = map(lambda a,b: a+b, l1, l2)
                print( list(l_sum) )

                # [2, 4, 6]
            

The filter() Function

The filter() Function

./images/filter.png
filter() is a built-in function that allows to process an iterable and extract those items that satisfy a given condition

            filter(function, iterable)
        
function - a function definition. Should return a Boolean value
iterable - a sequence (list, tupple, range or string) or any iterable object.
When you pass a function and an iterable to filter(), it applies the function to each item of the iterable and yields only those items for which the function returns True.
The filter() function returns an iterator that lazily evaluates each item in the iterable it processes
Example: List of even numbers with named function

                def is_even(x):
                    return x % 2 == 0


                evens = filter(is_even, range(1, 11))

                print(list(even_numbers))


                # [2, 4, 6, 8, 10]
            
Example: List of even numbers with lambda function

                evens = filter(lambda x: x % 2 == 0, range(1, 11))
                print(list(evens))

                # [2, 4, 6, 8, 10]
            

More examples

Example: filter empty strings.
If None is used for filter function, it defaults to Identity function - filters out elements that are False

                names = ["Ivan", "", "Alex", "", "Maria", "Angel", ""]
                not_empty_names = filter(None, names)

                print(list(not_empty_names))
                ["Ivan", "Alex", "Maria", "Angel"]
            
Note that we can use list comprehenstions insead of filter

                names = ["Ivan", "", "Alex", "", "Maria", "Angel", ""]
                not_empty_names = [ name for name in names if name]

                print( list(not_empty_names) )
            
Example: Names, starting with 'A'

                names = ["Ivan", "Alex", "Maria", "Angel", ""]
                filtered_names = filter(lambda name: name.startswith("A"), names)

                print(list(filtered_names))
            
With list comprehension

                names = ["Ivan", "Alex", "Maria", "Angel", ""]
                filtered_names = [name for name in names if name.startswith("A")]

                print(list(filtered_names))
            

The reduce() Function

The reduce() Function

./images/reduce.png
reduce() allows us to apply a function to an iterable and reduce it to a single cumulative value

                from functools import reduce

                reduce(function, iterable[, initializer])
            
In Python3, reduce() is not built-in, but we must import it from functools module!
function - a function definition.The first argument passed to function is the accumulated value and the second argument is the update value from the sequence
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5)
iterable - a sequence (list, tupple, range or string) or any iterable object.
If the optional initializer is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5], 100) calculates (((((100+1)+2)+3)+4)+5)

Examples

Example: sum the numbers from 0 to 10, including

                from functools import reduce

                res = reduce(lambda a,b: a+b, range(11))

                print(res)

                #55
            
Note, that in practice you can use the sum function:

                print(sum(range(11)))
            
Example: sum the prices of all products

                products = [
                    {'name':'apples', 'price': 2},
                    {'name':'oranges', 'price': 5},
                    {'name':'bananas', 'price': 3},
                ]

                #note that here we must provide initializer
                total_price = reduce( lambda price, product:price+product['price'], products, 0)

                print(total_price)
            
With sum function and list comprehension:

                total_price = sum([p["price"] for p in products])
            

Example: map - reduce

Often in practice filter, map and reduce are used in combination
Consider next example, which returns the sum of variable number of lists items positionally (element wise)

                from functools import reduce

                l1 = [1, 2, 3]
                l2 = [1, 2, 3]
                l3 = [1, 2, 3]

                res = map(lambda *t: reduce(lambda a, c: a + c, t), l1, l2, l3)
                print(list(res))

                # [3, 6, 9]
            
*t parameter in lambda collects in a tuple (t) all arguments. For example, on the first iteration, *t would collect the first element of each list into the tuple t=(1, 1, 1).
The reduce function applies its lambda to the tuple t, summing all its elements.

map/filter vs Comprehensions

map/filter vs Comprehensions

Why to use map/filter

While list/dictionary/set comprehensions can replace most uses of map and filter, there are still reasons why these functions remain useful:
Working with multiple iterables: map() can apply a function to elements from multiple sequences in parallel:

                list(map(lambda x, y: x + y, [1, 2, 3], [4, 5, 6]))  # [5, 7, 9]
            
Memory efficiency with large data: map() returns an iterator, not a list, so it's lazy-evaluated:

                # This doesn't build the whole list in memory
                result = map(process_function, very_large_dataset)
            

                filtered = filter(lambda x: x > 1000, huge_dataset)  # Iterator, not list
            

Why to use Comprehensions

Readability for simpler operation:

                squares = [x**2 for x in numbers]  # vs. map(lambda x: x**2, numbers)
            
When you need a list immediately: Comprehensions produce the full result at once

                squares = [x**2 for x in range(10)]  # Ready to use list
            
Combining mapping and filtering: Comprehensions handle this elegantly:

                even_squares = [x**2 for x in numbers if x % 2 == 0]
                # vs
                even_squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))
            

Homework

Homework

The tasks are given in next gist file
You can copy it and work directly on it. Just put your code under "### Your code here".

These slides are based on

customised version of

Hakimel's reveal.js

framework