Functions as First-Class Citizens

In Python, functions are more than just blocks of reusable code; in fact, they’re first-class citizens.

Python functions can be stored, passed them around, and even returned them from other functions. This might seem a bit weird at first, but it actually makes our code way more powerful and flexible.

What Does “First-Class Citizen” Mean?

When we say functions are first-class citizens in Python, we mean that functions can be:

  1. Assigned to variables
  2. Passed as arguments to other functions
  3. Returned from functions
  4. Stored in data structures

Examples:

1. Assigning Functions to Variables

1def greet(name):
2    return f"Hello, {name}!"
3
4# Assign the function to a variable
5my_greeting = greet
6
7# Use the variable to call the function
8print(my_greeting("Alice"))  
9# Hello, Alice!

2. Passing Functions as Arguments

 1def apply_operation(func, x, y):
 2    return func(x, y)
 3
 4def add(a, b):
 5    return a + b
 6
 7def multiply(a, b):
 8    return a * b
 9
10# Pass functions as arguments
11result1 = apply_operation(add, 5, 3)
12result2 = apply_operation(multiply, 5, 3)
13
14print(result1) 
15# 8
16print(result2)  
17#  15

3. Returning Functions from Functions

 1def create_multiplier(factor):
 2    def multiplier(x):
 3        return x * factor
 4    return multiplier
 5
 6double = create_multiplier(2)
 7triple = create_multiplier(3)
 8
 9print(double(5))  
10# 10
11print(triple(5))  
12# 15

4. Storing Functions in Data Structures

 1def square(x):
 2    return x ** 2
 3
 4def cube(x):
 5    return x ** 3
 6
 7# Store functions in a list
 8function_list = [square, cube]
 9
10for func in function_list:
11    print(func(3))  
12# 9, 27

Why This Matters

Treating functions as first-class citizens enables several powerful programming techniques:

  1. Higher-order functions: Functions that can accept other functions as arguments or return them. This is the basis for many functional programming concepts.
  2. Callback functions: Passing functions to be executed later, common in asynchronous programming.
  3. Decorators: A way to modify or enhance functions without changing their source code. Read: Python Function Decorators
  4. Lambda functions: Creating small, anonymous functions on the fly.
  5. Function factories: Functions that create and return other functions with specific behaviors. Read: Python @property decorator

Lambda Functions: Anonymous First-Class Citizens

Lambda functions, also known as anonymous functions, are a compact way to create small, one-time-use functions. They’re first-class citizens too, which means we can use them anywhere we’d use a regular function. This is particularly useful when we need a simple function for a short period.

Basic Lambda Syntax

1lambda arguments: expression

Let’s see how we can use lambdas in the contexts we discussed earlier:

1. Assigning Lambdas to Variables

 1square = lambda x: x**2
 2print(f"{square(5)}")  
 3# 25
 4
 5add = lambda x, y: x + y
 6print(f"{add(3, 5)}")  
 7# 8
 8
 9# Filtering numbers
10# prints out any number greater than 5 from a given list
11numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
12filtered = list(filter(lambda x: x > 5, numbers))
13print(f"{filtered}")  
14# [6, 7, 8, 9, 10]

2. Passing Lambdas as Arguments

 1def apply_operation(func, x, y):
 2    return func(x, y)
 3
 4# Using lambda instead of defined functions
 5result1 = apply_operation(lambda a, b: a + b, 5, 3)
 6result2 = apply_operation(lambda a, b: a * b, 5, 3)
 7
 8print(result1)  
 9# 8
10print(result2)  
11# 15

3. Returning Lambdas from Functions

 1def create_multiplier(factor):
 2    return lambda x: x * factor
 3
 4double = create_multiplier(2)
 5triple = create_multiplier(3)
 6
 7print(double(5))  
 8# 10
 9print(triple(5))  
10# 15

4. Storing Lambdas in Data Structures

 1# Store lambdas in a dictionary
 2operations = {
 3    'square': lambda x: x**2,
 4    'cube': lambda x: x**3,
 5    'double': lambda x: x*2
 6}
 7
 8print(operations['square'](3))  
 9# 9
10print(operations['cube'](3))    
11# 27
12print(operations['double'](3))  
13# 6

Common Use Cases for Lambdas

  1. With built-in functions:
1numbers = [1, 2, 3, 4, 5]
2squared = list(map(lambda x: x**2, numbers))
3print(squared)  
4# [1, 4, 9, 16, 25]
  1. In sorting operations:
1pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
2pairs.sort(key=lambda pair: pair[1])
3print(pairs)  
4# [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
  1. In list comprehensions:
1numbers = [1, 2, 3, 4, 5]
2odd_squares = [(lambda x: x**2)(x) for x in numbers if x % 2 != 0]
3print(odd_squares)  
4# [1, 9, 25]

Lambdas provide a concise way to create small, throwaway functions. They’re especially useful when you need a simple function for a short period, like in sorting operations or with higher-order functions like map() or filter().

Using Filter With Lambda

The filter() function selects elements from an iterable from a lambda or another function that returns True or False.

1# Returns even numbers from a list
2numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
4print(f"{even_numbers}")  
5# [2, 4, 6, 8, 10]

Using Map With Lambda

The map() function applies a given function to each item of an iterable and returns a map object.

1numbers = [1, 2, 3, 4, 5]
2squared_numbers = list(map(lambda x: x ** 2, numbers))
3print(f"{squared_numbers}") 
4# [1, 4, 9, 16, 25]

However, for more complex operations or reusable code, it’s often better to use regular named functions for clarity and maintainability.

By mastering lambdas along with other aspects of functions as first-class citizens, you can write more expressive and functional Python code.

Conclusion

Python’s treatment of functions as first-class citizens allows for more flexible, modular, and expressive code. By embracing this concept, you can write more elegant solutions to complex problems and tap into the full power of Python’s functional programming capabilities.