No history yet

Functional Python Patterns

A More Pythonic Way to Iterate

You're likely familiar with using for loops to build new lists. You initialize an empty list, loop through an existing collection, and append new values one by one. It works, but it's not the most direct way to express your intent.

# The traditional way
squares = []
for i in range(10):
    squares.append(i * i)

print(squares)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Python offers a more elegant and readable solution called a list comprehension. It lets you build a new list in a single, descriptive line of code. The structure is [expression for item in iterable].

# The comprehension way
squares = [i * i for i in range(10)]

print(squares)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

You can also include an if condition to filter elements, all within the same line. This powerful syntax extends to sets and dictionaries, too.

# Dictionary comprehension: {key_expr: val_expr for item in iterable}
num_to_square = {x: x**2 for x in range(5)}
# Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension: {expression for item in iterable}
# Note: This automatically handles duplicates
unique_letters = {letter for letter in 'hello world' if letter.isalpha()}
# Output: {'e', 'd', 'l', 'o', 'h', 'r', 'w'}

Functions as First-Class Citizens

In Python, functions are not just blocks of code; they are first-class citizens of the language. This means you can treat a function just like any other variable. You can pass it as an argument to another function, return it from a function, and store it in data structures like lists or dictionaries.

def shout(text):
    return text.upper() + '!'

def whisper(text):
    return text.lower() + '...'

def greet(func, message):
    # Here, 'func' is a function passed as an argument
    print(func(message))

greet(shout, 'Hello')    # Output: HELLO!
greet(whisper, 'Hello')  # Output: hello...

This ability is powerful, but defining a whole new function with def for a simple, one-time operation can feel clunky. For these situations, Python provides —small, anonymous functions defined with the lambda keyword.

A lambda function's syntax is lambda arguments: expression. The expression is evaluated and returned. They are limited to a single expression and cannot contain statements like assignments or loops.

# Using the 'greet' function from before with a lambda
greet(lambda x: x + '?', 'Are you sure')

# Output: Are you sure?

The Functional Toolbox

With functions as first-class citizens, we can use a set of powerful built-in tools for processing collections of data: map, filter, and reduce.

map(function, iterable) applies a function to every item of an iterable and returns a map object (an iterator) with the results.

numbers = [1, 2, 3, 4]

# Using map with a lambda function
doubled = map(lambda x: x * 2, numbers)

print(list(doubled)) # We convert the map object to a list to see the contents
# Output: [2, 4, 6, 8]

This is functionally equivalent to the list comprehension [x * 2 for x in numbers]. For simple cases, many Python developers prefer comprehensions for their readability.

filter(function, iterable) constructs an iterator from elements of an iterable for which the function returns True.

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

# Using filter with a lambda
evens = filter(lambda x: x % 2 == 0, numbers)

print(list(evens))
# Output: [2, 4, 6]

The final tool, reduce, is a bit different. It applies a function cumulatively to the items of a sequence to reduce it to a single value. It's not a built-in function anymore; you have to import it from the module.

from functools import reduce

numbers = [1, 2, 3, 4]

# Using reduce to find the sum of the list
product = reduce(lambda x, y: x + y, numbers)

print(product)
# Output: 10

Here, reduce first applies the lambda to 1 and 2 (result 3), then to 3 and 3 (result 6), and finally to 6 and 4, giving the final result of 10.

Memory-Efficient Iteration

List comprehensions are great, but they have one drawback: they create a new list in memory containing all the elements at once. If you're working with a very large sequence of data, this can consume a lot of RAM.

This is where come in. They look almost identical to list comprehensions but use parentheses instead of square brackets.

# A list comprehension (creates the full list in memory)
list_comp = [x*x for x in range(1000)]

# A generator expression (creates a generator object)
gen_expr = (x*x for x in range(1000))

print(list_comp[0]) # Instant access
# Output: 0

print(gen_expr) # Just shows the object, no values computed yet
# Output: <generator object <genexpr> at 0x...>

print(next(gen_expr)) # Computes and yields the first value
# Output: 0

A generator doesn't build the whole collection. Instead, it creates a special generator object that knows how to produce the values one by one, but only when you ask for them (for example, by using it in a for loop or calling next() on it). This lazy approach is perfect for building data processing pipelines that are both readable and memory-friendly.

Quiz Questions 1/6

Which of the following lines of code correctly creates a new list containing the squares of numbers from an existing list called my_list?

Quiz Questions 2/6

What does it mean for functions to be "first-class citizens" in Python?

These functional patterns allow you to write code that is often more concise and expressive. By focusing on what you want to do with your data rather than the step-by-step mechanics, you can create more readable and maintainable programs.