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
from functools import reduce, partial, lru_cache, wraps
β 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
Step by step:
1 + 2 = 3
3 + 3 = 6
6 + 4 = 10
10 + 5 = 15
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
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)
...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
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()
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
Use
partial()when: a function takes several arguments, but one of them is always the same in your use case. Instead of repeatingfunction(10, x)everywhere, createnew_function = partial(function, 10)once and callnew_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
With caching:
from functools import lru_cache
@lru_cache
def square(n):
print("Calculating...")
return n * n
square(5)
square(5)
square(5)
Calculating...
25
25
25
"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
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_cachecaches 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)
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)
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))
π 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()
Starting
Hello
Finished
The problem
print(greet.__name__)
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
Now greet.__name__ correctly stays "greet", and its docstring survives too.
π A simple analogy
Original gift:
π Name: Birthday Gift
Description: Something special
You wrap it in another box:
π¦
π
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
π§© 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
Top comments (0)