DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Week 04- Task 2.2: functools Toolkit: reduce, partial, lru_cache & wraps 🧰

functools is Python's built-in module for working with functions themselves β€” combining them, pre-filling their arguments, remembering their results, and preserving their identity through decorators. Let's go through all four.

Python
  β”‚
  β”œβ”€β”€ math
  β”œβ”€β”€ random
  β”œβ”€β”€ os
  └── functools   ← tools for working with functions
Enter fullscreen mode Exit fullscreen mode
from functools import reduce, partial, lru_cache, wraps
Enter fullscreen mode Exit fullscreen mode

βž• 1. reduce() β€” Combine everything into ONE

reduce() collapses a collection down to a single value by repeatedly combining items.

from functools import reduce

numbers = [1, 2, 3, 4, 5]
result = reduce(lambda a, b: a + b, numbers)
print(result)   # 15
Enter fullscreen mode Exit fullscreen mode

Step by step:

1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
Enter fullscreen mode Exit fullscreen mode

Syntax: reduce(function, iterable) β€” where a is the accumulated result so far, and b is the next item.

🧠 map vs filter vs reduce β€” the shape of the output

Direction Example ([1,2,3,4])
map() many β†’ many [2, 4, 6, 8]
filter() many β†’ fewer [2, 4]
reduce() many β†’ one 10

🟣 2. partial() β€” Pre-fill some arguments

Say you have a normal two-argument function:

def add(a, b):
    return a + b

print(add(10, 20))   # 30
Enter fullscreen mode Exit fullscreen mode

Now imagine you almost always want a to be 10. You could write a wrapper function manually:

def add_10(b):
    return add(10, b)
Enter fullscreen mode Exit fullscreen mode

...but partial() does this for you, cleanly:

from functools import partial

add_10 = partial(add, 10)

print(add_10(5))    # 15
print(add_10(20))   # 30
Enter fullscreen mode Exit fullscreen mode

add_10(5) is really just add(10, 5) under the hood β€” the first argument is permanently locked in.

Think of it like a form with a field you've already filled in:

Name: ______
Age:  ______
City: Bangalore   ← pre-filled by partial()
Enter fullscreen mode Exit fullscreen mode

Another example:

def multiply(a, b):
    return a * b

multiply_by_10 = partial(multiply, 10)

print(multiply_by_10(5))   # 50
print(multiply_by_10(7))   # 70
Enter fullscreen mode Exit fullscreen mode

Use partial() when: a function takes several arguments, but one of them is always the same in your use case. Instead of repeating function(10, x) everywhere, create new_function = partial(function, 10) once and call new_function(x).


πŸ”΅ 3. lru_cache() β€” Remember previous results

Caching = remembering answers you've already calculated, so you don't redo the work.

Without caching:

def square(n):
    print("Calculating...")
    return n * n

square(5)
square(5)
square(5)
# "Calculating..." prints all three times
Enter fullscreen mode Exit fullscreen mode

With caching:

from functools import lru_cache

@lru_cache
def square(n):
    print("Calculating...")
    return n * n

square(5)
square(5)
square(5)
Enter fullscreen mode Exit fullscreen mode
Calculating...
25
25
25
Enter fullscreen mode Exit fullscreen mode

"Calculating..." only prints once β€” every call after the first pulls the answer straight from the cache instead of recomputing it.

square(5) first call
   ↓
not in cache β†’ calculate β†’ 25 β†’ remember (5 β†’ 25)

square(5) again
   ↓
found in cache β†’ return 25, no calculation
Enter fullscreen mode Exit fullscreen mode

Why "LRU"?

LRU = Least Recently Used. If the cache has limited space and needs to make room for a new result, it evicts the entry that hasn't been used in the longest time.

One-liner for interviews: "lru_cache caches a function's results so repeated calls with the same arguments return instantly without recalculating."

πŸ”₯ Where it actually matters

Expensive, repeatable computations: recursion, dynamic programming, heavy math, or repeated lookups.

Classic case β€” Fibonacci:

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
Enter fullscreen mode Exit fullscreen mode

Plain recursion recalculates the same sub-values over and over:

fib(5)
 β”œβ”€β”€ fib(4)
 β”‚    β”œβ”€β”€ fib(3)
 β”‚    └── fib(2)
 └── fib(3)          ← calculated again!
      β”œβ”€β”€ fib(2)     ← calculated again!
      └── fib(1)
Enter fullscreen mode Exit fullscreen mode

Add caching, and each unique value is computed only once:

from functools import lru_cache

@lru_cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))
Enter fullscreen mode Exit fullscreen mode

🟠 4. wraps() β€” Preserve a function's identity through decorators

First, what's a decorator?

A function that adds behavior around another function, without touching its original code:

def decorator(func):
    def wrapper():
        print("Starting")
        func()
        print("Finished")
    return wrapper

@decorator
def greet():
    print("Hello")

greet()
Enter fullscreen mode Exit fullscreen mode
Starting
Hello
Finished
Enter fullscreen mode Exit fullscreen mode

The problem

print(greet.__name__)
Enter fullscreen mode Exit fullscreen mode

You'd expect "greet" β€” but you actually get "wrapper". Once decorated, greet now points to the inner wrapper function, so Python loses track of the original function's name, docstring, and other metadata.

The fix: @wraps(func)

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper
Enter fullscreen mode Exit fullscreen mode

Now greet.__name__ correctly stays "greet", and its docstring survives too.

🎁 A simple analogy

Original gift:

🎁 Name: Birthday Gift
   Description: Something special
Enter fullscreen mode Exit fullscreen mode

You wrap it in another box:

πŸ“¦
  🎁
Enter fullscreen mode Exit fullscreen mode

Without wraps(), Python thinks the outer box is the gift and forgets the label. With wraps(), you tell Python: "There's a wrapper here, but keep the original gift's identity."

The standard pattern you'll see everywhere

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # extra behavior here
        result = func(*args, **kwargs)
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

🧩 Putting It All Together

Tool Purpose Example
reduce() Combine many values into one reduce(lambda a,b: a+b, [1,2,3,4]) β†’ 10
partial() Pre-fill some arguments of a function partial(multiply, 2)(5) β†’ 10
lru_cache() Remember previous results, skip recomputation @lru_cache on square(n)
wraps() Preserve a function's metadata inside a decorator @wraps(func) inside wrapper
                 functools
                     β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚             β”‚              β”‚
     reduce       partial       lru_cache
       β”‚             β”‚              β”‚
   many β†’ ONE    pre-fill       remember
                  arguments      results
       β”‚             β”‚              β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
                   wraps
                     β”‚
             preserve function
                information
Enter fullscreen mode Exit fullscreen mode

Top comments (0)