11 Prepare: Functional Programming
A paradigm is a way of thinking or a way of perceiving the world. There are at least four main paradigms for programming a computer: procedural, declarative, functional, and object-oriented. During previous lessons in CSE 110 and 111, you used procedural programming. During this lesson, you will be introduced to functional programming. If you enroll in CSE 382 - Patterns in Functional Programming, you will study functional programming in depth.
Procedural programming is a programming paradigm that focuses on the process or the steps to accomplish a task. This is the type of programming that you did in CSE 110 and in previous lessons of CSE 111.
Declarative programming is a programming paradigm that does not focus on the process or steps to accomplish a task. Instead, with declarative programming, a programmer focuses on what she wants from the computer, or in other words, she focuses on the desired results. The SQL programming language is a good example of a declarative language. If you have ever written SQL code, then you have used declarative programming. When writing SQL code, a programmer writes code to tell the computer what she wants in the results but not the steps the computer must follow to get those results.
Functional programming is a programming paradigm that focuses on functions and avoids shared state, mutating state, and side effects. There are many techniques and concepts that are part of functional programming. However, in this lesson we will focus on just three, namely:
- We can pass a function into another function.
- A nested function is a function defined inside another function.
- A lambda function is a small anonymous function.
Concepts
Here are the functional programming concepts that you should learn during this lesson.
Passing a Function into another Function
The Python programming language allows a programmer to pass a
function as an argument into another function. A function that
accepts other functions in its parameters is known as a
higher-order function. Higher-order functions are often
used to process the elements in a list. Before seeing an example of
using a higher-order function to process a list, first consider the
program in example 1 that doesn’t use a higher-order function
but instead uses a for
loop to convert a list of
temperatures from Fahrenheit to Celsius.
# Example 1 def main(): fahr_temps = [72, 65, 71, 75, 82, 87, 68] # Print the Fahrenheit temperatures. print(f"Fahrenheit: {fahr_temps}") # Convert each Fahrenheit temperature to Celsius and store # the Celsius temperatures in a list named cels_temps. cels_temps = [] for fahr in fahr_temps: cels = cels_from_fahr(fahr) cels_temps.append(cels) # Print the Celsius temperatures. print(f"Celsius: {cels_temps}") def cels_from_fahr(fahr): """Convert a Fahrenheit temperature to Celsius and return the Celsius temperature. """ cels = (fahr - 32) * 5 / 9 return round(cels, 1) # Call main to start this program. if __name__ == "__main__": main()
> python example_1.py Fahrenheit: [72, 65, 71, 75, 82, 87, 68] Celsius: [22.2, 18.3, 21.7, 23.9, 27.8, 30.6, 20.0]
At lines 12–14
in example 1, there is a for
loop that converts
each Fahrenheit temperature to Celsius and then appends the Celsius
temperature onto a new list. Writing a for
loop like
this is the traditional way to process all the elements in a list
and doesn’t use higher-order functions.
Python includes a built-in higher-order function named
map
that will process all the elements in a list and
return a new list that contains the results. The
map
function
accepts a function and a list as arguments and contains a loop
inside it, so that when a programmer calls the map
function, he doesn’t need to write a loop. The map
function is a higher-order function because it accepts a function as
an argument. Consider the program in example 2 that produces
the same results as example 1.
# Example 2 def main(): fahr_temps = [72, 65, 71, 75, 82, 87, 68] # Print the Fahrenheit temperatures. print(f"Fahrenheit: {fahr_temps}") # Convert each Fahrenheit temperature to Celsius and store # the Celsius temperatures in a list named cels_temps. cels_temps = list(map(cels_from_fahr, fahr_temps)) # Print the Celsius temperatures. print(f"Celsius: {cels_temps}") def cels_from_fahr(fahr): """Convert a Fahrenheit temperature to Celsius and return the Celsius temperature. """ cels = (fahr - 32) * 5 / 9 return round(cels, 1) # Call main to start this program. if __name__ == "__main__": main()
> python example_2.py Fahrenheit: [72, 65, 71, 75, 82, 87, 68] Celsius: [22.2, 18.3, 21.7, 23.9, 27.8, 30.6, 20.0]
Notice that example 2, doesn’t contain a for
loop. Instead, at
line 11,
it contains a call to the map
function. Remember that
the map
function has a loop inside it, so that the
programmer who calls map
, doesn’t have to write the
loop. Notice also at
line 11
that the first argument to the map
function is the name
of the cels_from_fahr
function. In other words, at
line 11, we are passing the cels_from_fahr
function into the map
function, so that
map
will call cels_from_fahr
for each
element in the fahr_temps list.
The map
function is just one example of a
higher-order function. Python also includes the built-in
higher-order
sorted
and
filter
functions and several higher-order functions in the
functools
module.
Nested Functions
The Python programming language allows a programmer to define
nested functions. A nested function is a function that is
defined inside another function and is useful when we wish to split
a large function into smaller functions and the smaller functions
will be called by the containing function only. The program in
example 3 produces the same results as examples 1
and 2, but it uses a nested function. Notice in example 3
at lines 5–10
that the cels_from_fahr
function is nested inside the
main
function.
# Example 3 def main(): def cels_from_fahr(fahr): """Convert a Fahrenheit temperature to Celsius and return the Celsius temperature. """ cels = (fahr - 32) * 5 / 9 return round(cels, 1) fahr_temps = [72, 65, 71, 75, 82, 87, 68] # Print the Fahrenheit temperatures. print(f"Fahrenheit: {fahr_temps}") # Convert each Fahrenheit temperature to Celsius and store # the Celsius temperatures in a list named cels_temps. cels_temps = list(map(cels_from_fahr, fahr_temps)) # Print the Celsius temperatures. print(f"Celsius: {cels_temps}") # Call main to start this program. if __name__ == "__main__": main()
> python example_3.py Fahrenheit: [72, 65, 71, 75, 82, 87, 68] Celsius: [22.2, 18.3, 21.7, 23.9, 27.8, 30.6, 20.0]
Lambda Functions
A Python lambda function is a small
anonymous function, meaning a small function without a name. A
lambda function is always a small function because the Python
language restricts a lambda function to just one expression.
Consider the program in example 4 which is yet another example
program that converts Fahrenheit temperatures to Celsius. Notice the
lambda function at line 12
of example 4. It takes one parameter named fahr and
computes and returns the corresponding Celsius temperature. At
line 16, the lambda
function is passed into the map
function.
# Example 4 def main(): fahr_temps = [72, 65, 71, 75, 82, 87, 68] # Print the Fahrenheit temperatures. print(f"Fahrenheit: {fahr_temps}") # Define a lambda function that converts # a Fahrenheit temperature to Celsius and # returns the Celsius temperature. cels_from_fahr = lambda fahr: round((fahr - 32) * 5 / 9, 1) # Convert each Fahrenheit temperature to Celsius and store # the Celsius temperatures in a list named cels_temps. cels_temps = list(map(cels_from_fahr, fahr_temps)) # Print the Celsius temperatures. print(f"Celsius: {cels_temps}") # Call main to start this program. if __name__ == "__main__": main()
> python example_4.py Fahrenheit: [72, 65, 71, 75, 82, 87, 68] Celsius: [22.2, 18.3, 21.7, 23.9, 27.8, 30.6, 20.0]
Some students are confused by the statement that a lambda
function is an anonymous function (a function without a name).
Looking at the lambda function in example 4 at
line 12, it appears
that the lambda function is named cels_from_fahr.
However, cels_from_fahr is the name of a variable, not
the name of the lambda function. The lambda function has no name.
This distinction may seem trivial until we see an example of an
inline lambda function. Notice in the next example that the lambda
function is defined inside the parentheses for the call to the
map
function.
# Convert each Fahrenheit temperature to Celsius and store # the Celsius temperatures in a list named cels_temps. cels_temps = list(map( lambda fahr: round((fahr - 32) * 5 / 9, 1), fahr_temps))
To write a lambda function write code that follows this template:
lambda param1, param2, … paramN: expression
As shown in the template, type the keyword lambda
,
then parameters separated by commas, then a colon (:), and finally
an expression that performs arithmetic, modifies a string, or
computes something else.
In Python, every lambda function can be written as a regular
Python function. For example, the lambda function in example 4
can be rewritten as the cels_from_fahr
function in
examples 1, 2, and 3.
Example - Map and Filter
The checkpoint for lesson 9 required you to write a program that replaced all the occurrences of "AB" in a list with the name "Alberta" and then counted how many times the name "Alberta" appeared in the list.
Example 5 contains a program that uses the map
and filter
functions to complete the requirements of
the lesson 9 checkpoint.
The example program works by doing the following:
- Calling the
read_list
function at line 6 to read all the provinces from a text file into a list. (Theread_list
function is in the preparation content for lesson 9.) - Calling the
map
function at line 22 to convert all elements that are "AB" to "Alberta." - Calling the
filter
function at line 34 to remove all elements that are not "Alberta." - Calling the
len
function at line 42 to count the number of elements that remain in the filtered list.
# Example 5 def main(): # Read a file that contains a list # of Canadian province names. provinces_list = read_list("provinces.txt") # As a debugging aid, print the entire list. print("Original list of provinces:") print(provinces_list) print() # Define a nested function that converts AB to Alberta. def alberta_from_ab(province_name): if province_name == "AB": province_name = "Alberta" return province_name # Replace all occurrences of "AB" with "Alberta" by # calling the map function and passing the ablerta_from_ab # function and provinces_list into the map function. new_list = list(map(alberta_from_ab, provinces_list)) print("List of provinces after AB was changed to Alberta:") print(new_list) print() # Define a lambda function that returns True if a # province's name is Alberta and returns False otherwise. is_alberta = lambda name: name == "Alberta" # Filter the new list to only those provinces that # are "Alberta" by calling the filter function and # passing the is_alberta function and new_list. filtered_list = list(filter(is_alberta, new_list)) print("List filtered to Alberta only:") print(filtered_list) print() # Because all the elements in filtered_list are # "Alberta", we can count how many elements are # "Alberta" by simply calling the len function. count = len(filtered_list) print(f"Alberta occurs {count} times in the modified list.") # Call main to start this program. if __name__ == "__main__": main()
> python example_5.py Original list of provinces: ['Alberta', 'Ontario', 'Prince Edward Island', 'Ontario', 'Quebec', 'Saskatchewan', 'AB', 'Nova Scotia', 'Alberta', 'Northwest Territories', 'Saskatchewan', 'Nunavut', 'Nova Scotia', 'Prince Edward Island', 'Alberta', 'Nova Scotia', 'Nova Scotia', 'Prince Edward Island', 'British Columbia', 'Ontario', 'Ontario', 'Newfoundland and Labrador', 'Ontario', 'Ontario', 'Saskatchewan', 'Nova Scotia', 'Prince Edward Island', 'Saskatchewan', 'Ontario', 'Newfoundland and Labrador', 'Ontario', 'British Columbia', 'Manitoba', 'Ontario', 'Alberta', 'Saskatchewan', 'Ontario', 'Yukon', 'Ontario', 'New Brunswick', 'British Columbia', 'Manitoba', 'Yukon', 'British Columbia', 'Manitoba', 'Yukon', 'Newfoundland and Labrador', 'Ontario', 'Yukon', 'Ontario', 'AB', 'Nova Scotia', 'Newfoundland and Labrador', 'Yukon', 'Nunavut', 'Northwest Territories', 'Nunavut', 'Yukon', 'British Columbia', 'Ontario', 'AB', 'Saskatchewan', 'Prince Edward Island', 'Saskatchewan', 'Prince Edward Island', 'Alberta', 'Ontario', 'Alberta', 'Manitoba', 'AB', 'British Columbia', 'Alberta'] List of provinces after AB was changed to Alberta: ['Alberta', 'Ontario', 'Prince Edward Island', 'Ontario', 'Quebec', 'Saskatchewan', 'Alberta', 'Nova Scotia', 'Alberta', 'Northwest Territories', 'Saskatchewan', 'Nunavut', 'Nova Scotia', 'Prince Edward Island', 'Alberta', 'Nova Scotia', 'Nova Scotia', 'Prince Edward Island', 'British Columbia', 'Ontario', 'Ontario', 'Newfoundland and Labrador', 'Ontario', 'Ontario', 'Saskatchewan', 'Nova Scotia', 'Prince Edward Island', 'Saskatchewan', 'Ontario', 'Newfoundland and Labrador', 'Ontario', 'British Columbia', 'Manitoba', 'Ontario', 'Alberta', 'Saskatchewan', 'Ontario', 'Yukon', 'Ontario', 'New Brunswick', 'British Columbia', 'Manitoba', 'Yukon', 'British Columbia', 'Manitoba', 'Yukon', 'Newfoundland and Labrador', 'Ontario', 'Yukon', 'Ontario', 'Alberta', 'Nova Scotia', 'Newfoundland and Labrador', 'Yukon', 'Nunavut', 'Northwest Territories', 'Nunavut', 'Yukon', 'British Columbia', 'Ontario', 'Alberta', 'Saskatchewan', 'Prince Edward Island', 'Saskatchewan', 'Prince Edward Island', 'Alberta', 'Ontario', 'Alberta', 'Manitoba', 'Alberta', 'British Columbia', 'Alberta'] List filtered to Alberta only: ['Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta', 'Alberta'] Alberta occurs 11 times in the modified list.
Example - Sorting a Compound List
Python includes a built-in higher-order function named
sorted
that accepts a list as an argument and returns a
new sorted list. Calling the sorted
function is
straightforward for a simple list such as a list of strings or a
list of numbers as shown in example 6 and its output.
# Example 6 def main(): # Create a list that contains country names # and print the list. countries = [ "Canada", "France", "Ghana", "Brazil", "Japan" ] print(countries) # Sort the list. Then print the sorted list. sorted_list = sorted(countries) print(sorted_list) # Call main to start this program. if __name__ == "__main__": main()
> python countries.py ['Mexico', 'France', 'Ghana', 'Brazil', 'Japan'] ['Brazil', 'France', 'Ghana', 'Japan', 'Mexico']
A compound list is a list that contains lists. Sorting a compound list is more complex than sorting a simple list. Consider this compound list that contains data about some countries.
# Create a list that contains data about countries. countries = [ # [country_name, land_area, population, gdp_per_capita] ["Mexico", 1972550, 126014024, 21362], ["France", 640679, 67399000, 45454], ["Ghana", 239567, 31072940, 7343], ["Brazil", 8515767, 210147125, 14563], ["Japan", 377975, 125480000, 41634] ]
Perhaps we want the countries compound list sorted by
country name or perhaps we want it sorted by population. The element
that we want a list sorted by is known as the key
element. If we want to use the sorted
function to
sort a compound list, we must tell the sorted
function
which element is the key element, which we do by passing a small
function as an argument into the sorted
function. This
small function is called the key function and extracts
the key element from a list as shown in example 7.
Notice at line 26
in example 7, there is a lambda function that extracts the
population from a country. Then at
line 29 that lambda
function is passed to the sorted
function so that the
sorted
function will sort the list of countries by the
population.
# Example 7 def main(): # Create a list that contains data about countries. countries = [ # [country_name, land_area, population, gdp_per_capita] ["Mexico", 1972550, 126014024, 21362], ["France", 640679, 67399000, 45454], ["Ghana", 239567, 31072940, 7343], ["Brazil", 8515767, 210147125, 14563], ["Japan", 377975, 125480000, 41634] ] # Print the unsorted list. print("Original unsorted list of countries") for country in countries: print(country) print() # Define a lambda function that will be used as the # key function by the sorted function. The lambda # function extracts the population data from a # country so that the population will be used for # sorting the list of countries. POPULATION_INDEX = 2 popul_func = lambda country: country[POPULATION_INDEX] # Sort the list of countries by the population. sorted_list = sorted(countries, key=popul_func) # Print the sorted list. print("List of countries sorted by population") for country in sorted_list: print(country) # Call main to start this program. if __name__ == "__main__": main()
> python countries.py Original unsorted list of countries ['Mexico', 1972550, 126014024, 21362] ['France', 640679, 67399000, 45454] ['Ghana', 239567, 31072940, 7343] ['Brazil', 8515767, 210147125, 14563] ['Japan', 377975, 125480000, 41634] List of countries sorted by population ['Ghana', 239567, 31072940, 7343] ['France', 640679, 67399000, 45454] ['Japan', 377975, 125480000, 41634] ['Mexico', 1972550, 126014024, 21362] ['Brazil', 8515767, 210147125, 14563]
By using a key function it’s possible to sort a compound list
with a key element that isn’t in the list. Consider the compound
list named students that contains data about various
students in example 8. Within the list, each student’s
given name and surname are stored separately. It is common for a
user to want such a list to be sorted by surname and then by
given name. A simple way to do that is to write a key function
that combines the surname and given name elements and returns
the combined name as the key that the sorted
function
will use for sorting.
Lines 21–22
in example 8 contain a lambda
function that
combines a student’s surname and given name into a string that is
used as the key by the sorted function at
line 25. Notice in
the output from example 8 that the students are sorted by
surname and then by given name.
# Example 8 def main(): # Create a list that contains data about young students. students = [ # [given_name, surname, reading_level] ["Robert", "Smith", 6.7], ["Annie", "Smith", 6.2], ["Robert", "Lopez", 7.1], ["Sean", "Li", 5.6], ["Sofia", "Lopez", 5.3], ["Lily", "Harris", 6.7], ["Alex", "Harris", 5.8] ] GIVEN_INDEX = 0 SURNAME_INDEX = 1 # Define a lambda function that combines # a student's surname and given name. combine_names = lambda student_list: \ f"{student_list[SURNAME_INDEX]}, {student_list[GIVEN_INDEX]}" # Sort the list by the combined key of surname, given_name. sorted_list = sorted(students, key=combine_names) # Print the list. for student in sorted_list: print(student) # Call main to start this program. if __name__ == "__main__": main()
> python students.py ['Alex', 'Harris', 5.8] ['Lily', 'Harris', 6.7] ['Sean', 'Li', 5.6] ['Robert', 'Lopez', 7.1] ['Sofia', 'Lopez', 5.3] ['Annie', 'Smith', 6.2] ['Robert', 'Smith', 6.7]
Videos
These two Microsoft videos might help you understand lambda functions more deeply.
- Lambda Functions (5 minutes)
- Demo: Lambda Functions (4 minutes)
If you wish to learn more about functional programming in Python, watch these videos by Dan Bader.
- The Basics of Functional Programming in Python (19 minutes)
- The Python
filter
function (16 minutes) - The Python
map
function (14 minutes) - The Python
reduce
function (18 minutes)
Additional Documentation
If you wish to learn even more details about functional programming, the following articles contain reference documentation for functional programming in Python.
- Python
map
function - Python
filter
function - Python
reduce
function - Python
functools
module - A thorough tutorial about lambda functions
Summary
In this preparation content, you learned that functional programming is a programming paradigm that focuses on functions, and you learned these three concepts that are used in functional programming:
- You can pass a function as an argument into another function.
- A nested function is a function defined inside another function.
- A lambda function is a small anonymous function.