Hello! Welcome to the first lesson in our course on advanced Python.
Your goal is to become comfortable with Python code that uses features like context managers, decorators, and type hints. To get there, we need to start with a fundamental concept that underpins many of Python's powerful features: the idea that functions are first-class objects.
In this lesson, we will explore what it means for functions to be "first-class citizens" in Python. Specifically, you will learn how to:
- Assign functions to variables.
- Store functions in data structures like lists.
- Pass functions as arguments to other functions.
This concept is the first step toward understanding decorators and other advanced patterns we'll encounter later in the course. Given your background in Java, you might find this similar to how Java 8+ handles lambda expressions and functional interfaces, but Python's approach is arguably more direct.
Let's begin.
What Are First-Class Functions?
In a programming language, the term "first-class citizen" (or first-class object) refers to an entity that can be treated like any other value. This means it can be stored in a variable, passed as an argument to a function, or returned from a function. In Python, functions have this status.
To get a clear definition of this concept, please watch the first part of the following video from Corey Schafer.
Programming Terms: First-Class Functions
This video provides a concise definition of first-class functions.
Watch the section 'What is a First-Class Function?' from the beginning until 01:23.
As the video explains, this means we can work with functions in the same way we work with strings, numbers, or lists. Let's see this in action.
Functions as Objects
Every function you define in Python is an object. This is a crucial point. Consider this simple function:
def greet(name):
return f"Hello, {name}"
The name greet is a variable that refers to a function object. If you check its type, you'll see it's a function:
print(type(greet))
# <class 'function'>
This is different from greet("World"), which is the result of calling the function—in this case, the string "Hello, World". The distinction between the function object (greet) and the result of its execution (greet(...)) is key.
1. Assigning Functions to Variables
Because functions are objects, you can assign them to other variables. This is not a copy of the function, but a new reference to the same underlying function object.
To explore this, please read the first two sections of the following article from dbader.org.
Python's Functions Are First-Class – dbader.org
This article, 'Python's Functions Are First-Class', uses a clear example to demonstrate how functions behave as objects.
Read the sections titled 'Functions Are Objects'. Pay close attention to the yell and bark example, including what happens when the original name yell is deleted.
As you saw, bark = yell creates a new name, bark, that points to the same function. You can call the function using either name.
2. Storing Functions in Data Structures
Since functions are objects, you can also store them in data structures like lists or dictionaries.
def add(a, b):
return a + b
def subtract(a, b):
return a - b
operations = [add, subtract]
# Call the first function in the list
result = operations[0](10, 5) # This is equivalent to add(10, 5)
print(result) # Output: 15
This allows you to create flexible and dynamic code. For instance, you could select an operation to perform at runtime based on user input or some other condition.
The article you just started reading also has a great section on this.
Python's Functions Are First-Class – dbader.org
Let's continue with the same article to see how functions can be stored in data structures.
Now, read the section titled 'Functions Can Be Stored In Data Structures'.
Passing Functions as Arguments
One of the most powerful consequences of first-class functions is the ability to pass them as arguments to other functions. Functions that accept other functions as arguments are known as higher-order functions.
This is a pattern you might recognize from Java, where you can pass an object implementing a functional interface (or a lambda) to a method like Collections.sort() or a stream's map() method. In Python, this is a natural extension of functions being objects.
The following video demonstrates this by building a custom map function. This is a classic example that clearly shows the power of higher-order functions.
Programming Terms: First-Class Functions
Let's return to Corey Schafer's video to see a practical example of passing a function as an argument.
Watch the section 'Passing Functions as Arguments' from 03:41 to 07:23. Focus on how the my_map function is defined to accept a function (func) and how it's used with both square and cube.
The ability to pass behavior (the square or cube function) into a generic algorithm (the my_map function) is a cornerstone of functional programming and a technique used extensively in Python.
Practice Exercise
Now, let's put these concepts into practice.
-
Create two functions:
to_lowercase(s)that takes a string and returns it in all lowercase.add_enthusiasm(s)that takes a string and appends three exclamation marks (!!!) to it.
-
Create a higher-order function named
process_string(s, processor)that takes a stringsand a functionprocessoras arguments. This function should call theprocessorfunction with the stringsand print the result. -
Test your
process_stringfunction by calling it with each of the functions you created in step 1.
Click here to see a possible solution
# Step 1: Define the processing functions
def to_lowercase(s):
"""Converts a string to lowercase."""
return s.lower()
def add_enthusiasm(s):
"""Adds three exclamation marks to a string."""
return s + '!!!'
# Step 2: Define the higher-order function
def process_string(s, processor):
"""
Processes a string using a given processor function and prints the result.
Args:
s (str): The string to process.
processor (callable): A function that takes a string and returns a string.
"""
processed_s = processor(s)
print(processed_s)
# Step 3: Test the implementation
my_string = "This Is a Test"
print("Processing with to_lowercase:")
process_string(my_string, to_lowercase) # Expected output: this is a test
print("\nProcessing with add_enthusiasm:")
process_string(my_string, add_enthusiasm) # Expected output: This Is a Test!!!
Conclusion
In this lesson, we established that Python's functions are first-class objects. This is not just a theoretical detail; it has practical implications that enable powerful programming patterns.
Here are the key takeaways:
- Functions can be treated like any other data type in Python.
- You can assign a function to a variable using its name without parentheses (e.g.,
my_var = my_func). - You can store functions in data structures like lists and dictionaries.
- You can pass functions as arguments to other functions, which are then called "higher-order functions."
Understanding this concept is essential, as it forms the foundation for many of the topics we'll cover next.
In our next lesson, we will build directly on this by exploring nested functions—functions defined inside other functions. This will lead us to the concept of closures, which are critical for understanding decorators.