Python Programming from First Principles: Learn to Code, Think Computationally, and Solve Real-World Problems
PDFEPUB
BOOK 000 · CHAPTER 5 · Lambda Functions, Recursion, and Generators

Lambda Functions, Recursion, and Generators

On this pageLambda Functions — Quick, Anonymous FunctionsRecursion — A Function That Calls ItselfGenerators — Producing Data EfficientlyExampleUsing next() ManuallyWhy Use Generators?Mini Project — Random Quote Stream

As you become more comfortable with Python functions, you’ll discover that they can be written and used in different ways.

In this section, we explore three powerful techniques that make Python functions more expressive and efficient: lambda functions, recursion, and generators.

Lambda Functions — Quick, Anonymous Functions

Sometimes, you need a small function only once, such as when sorting data or filtering a list. Instead of writing a full def block, Python allows you to create anonymous functions using the lambda keyword.

A lambda function can have any number of arguments, but only one expression. The expression is automatically returned.

square = lambda x: x ** 2
print(square(5))
Listing 230 — Lambda Functions — Quick, Anonymous Functions

Output

25

This is equivalent to:

def square(x):
    return x ** 2
Listing 231 — Lambda Functions — Quick, Anonymous Functions

Lambda functions are often used with built-in functions like map(),filter(), or sorted().

For example:

numbers = [3, 6, 9, 2, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled)
Listing 232 — Lambda Functions — Quick, Anonymous Functions

Output

[6, 12, 18, 4, 10]

Here,map() applies the lambda function to each element of the list — doubling all numbers without needing a separate function definition.

Recursion — A Function That Calls Itself

Recursion is a technique where a function calls itself to solve a smaller version of the same problem. It is often used in mathematical operations or tasks that naturally repeat with smaller inputs — such as calculating factorials or navigating nested data.

For a nonnegative integer n, the factorial is the product of the integers from 1 to n; 0! is 1. This example assumes a nonnegative integer input.

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)
Listing 233 — Recursion — A Function That Calls Itself
print(factorial(5))
Listing 234 — Recursion — A Function That Calls Itself

Output

120

Here’s what happens:

1.factorial(5) calls factorial(4) 2.factorial(4) calls factorial(3) and so on 3. When n == 0, the recursion stops (base case) 4. The results are multiplied as the calls return.

It’s important to always include a base case, otherwise the function will call itself forever and cause a recursion error.

Generators — Producing Data Efficiently

Generators are functions that can pause and resume their execution.

When a generator function is called, it returns a generator object, which is an iterator.

Its body has not started running. Execution begins when a value is requested, through iteration or next().

yield supplies a value and pauses execution. A return or the end of the body finishes the generator.

Example

Here’s an example that generates numbers up to a limit:

def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count += 1

# Create a generator
numbers = count_up_to(5)

# Iterate through values one by one
for num in numbers:
    print(num)
Listing 235 — Example

Output

1
2
3
4
5

Here’s what happens step by step:

  • The function count_up_to() is called, but it doesn’t run completely—it returns a generator object.
  • Each iteration in the for loop triggers the next yield statement.
  • The function pauses after yielding each number, and resumes the next time the loop requests a new value.

Using next() Manually

You can also control generators manually with the next() function, which retrieves the next yielded value each time it’s called.

gen = count_up_to(3)
print(next(gen))
print(next(gen))
print(next(gen))
Listing 236 — Using next() Manually

Output

1
2
3

Why Use Generators?

Generators are memory-efficient because they produce one value at a time instead of creating an entire list in memory. This makes them especially powerful when working with large sequences or infinite streams of data.

For instance, compare a generator to building a list directly:

# Regular list - builds all items at once
nums_list = [i for i in range(1000000)]

# Generator - builds items on demand
nums_gen = (i for i in range(1000000))
Listing 237 — Why Use Generators?

The list version stores one million numbers in memory.

The generator version, using parentheses ( ), produces numbers on demand, saving resources.

Mini Project — Random Quote Stream

Let’s use a generator to simulate an ongoing stream of random quotes every time the user requests one. It combines your knowledge of functions, generators, and the random module.

import random

def quote_stream():
    quotes = [
        "Keep learning, keep growing.",
        "Code is like humor — when you have to explain it, it’s bad.",
        "Simplicity is the soul of efficiency.",
        "In theory, there’s no difference between theory and practice."
    ]
    while True:
        yield random.choice(quotes)

# Create a generator
stream = quote_stream()

# Get three random quotes
print(next(stream))
print(next(stream))
print(next(stream))
Listing 238 — Mini Project — Random Quote Stream

Output — one possible run

Simplicity is the soul of efficiency.
Keep learning, keep growing.
Simplicity is the soul of efficiency.

Each call to next(stream) retrieves a new random quote, while the generator keeps running indefinitely until stopped.

This demonstrates how generators can power infinite or dynamic data flows efficiently.