description: "Functional Programming — A function in Python is just another value: it can be assigned to a variable, stored in a data structure, or passed as an…"---
03 · Functional Programming¶
🎥 Video walkthrough¶
Python isn't a purely functional language, but it borrows some of the most
useful ideas from that world: treating functions as values, transforming data
with map/filter/reduce, and writing small anonymous functions with
lambda. These tools pair naturally with the comprehensions from the previous
module.
Functions are first-class objects¶
A function in Python is just another value: it can be assigned to a variable, stored in a data structure, or passed as an argument.
def shout(text):
return text.upper() + "!"
def whisper(text):
return text.lower() + "..."
greeting = shout # no parentheses — this stores the function itself
print(greeting("hello")) # HELLO!
functions = [shout, whisper]
for fn in functions:
print(fn("Hi there"))
Higher-order functions¶
A higher-order function takes another function as an argument, or returns one.
def apply_twice(fn, value):
return fn(fn(value))
def add_ten(x):
return x + 10
print(apply_twice(add_ten, 5)) # 25
lambda — small anonymous functions¶
lambda creates a single-expression function inline, useful when a full def
would be overkill — most commonly as a key= argument.
square = lambda x: x * x
print(square(6)) # 36
people = [{"name": "Ada", "age": 36}, {"name": "Grace", "age": 85}]
people.sort(key=lambda p: p["age"])
print([p["name"] for p in people]) # ['Ada', 'Grace']
Keep lambdas short. If the logic needs a comment or more than one expression,
write a regular def function instead — it will be easier to read and debug.
map — transform every element¶
prices = [19.99, 5.50, 3.25]
with_tax = list(map(lambda p: round(p * 1.08, 2), prices))
print(with_tax) # [21.59, 5.94, 3.51]
# equivalent, and usually more Pythonic:
with_tax = [round(p * 1.08, 2) for p in prices]
filter — keep elements matching a condition¶
words = ["apple", "kiwi", "fig", "banana", "pear"]
short_words = list(filter(lambda w: len(w) <= 4, words))
print(short_words) # ['kiwi', 'fig', 'pear']
# equivalent comprehension form:
short_words = [w for w in words if len(w) <= 4]
functools.reduce — fold a sequence into one value¶
reduce isn't a builtin — it lives in functools because it's less commonly
needed and can hurt readability if overused.
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, n: acc + n, numbers) # 15
product = reduce(lambda acc, n: acc * n, numbers, 1) # 120, with explicit start value
# for simple cases, builtins are clearer:
total = sum(numbers)
Closures — functions that remember their environment¶
def make_multiplier(factor):
def multiplier(x):
return x * factor # "factor" is captured from the enclosing scope
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
double and triple are both built from the same inner function, but each
one remembers its own factor — that's a closure.
A first look at decorators¶
A decorator is a higher-order function that wraps another function to add behavior before/after it runs, without changing the original function's code. We'll go much deeper into decorators in Level 3; this is the shape to recognize.
import time
def timed(fn):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.6f}s")
return result
return wrapper
@timed
def slow_square(n):
total = 0
for i in range(n):
total += i * i
return total
slow_square(1_000_000)
# slow_square took 0.0XXXXXs
@timed above def slow_square is exactly equivalent to writing
slow_square = timed(slow_square) right after the function is defined.
Cheat sheet¶
| Tool | Purpose | Comprehension equivalent |
|---|---|---|
map(fn, iterable) |
transform each item | [fn(x) for x in iterable] |
filter(fn, iterable) |
keep matching items | [x for x in iterable if fn(x)] |
functools.reduce(fn, iterable) |
fold into one value | usually a for loop or sum/max |
lambda args: expr |
inline single-expression function | — |
How It Actually Works¶
"Functions are first-class" is literal: a function is an ordinary heap
object of type function, carrying __code__ (the compiled bytecode),
__defaults__, __globals__ (the module namespace it was defined in), and
__closure__. Assigning greeting = shout just copies a pointer; there is no
special "function variable".
A closure is a tuple of cell objects. When the compiler sees that
multiplier reads factor from its enclosing function, it doesn't copy the
value — it compiles factor in make_multiplier as a cell variable
(a tiny box object) instead of a normal local. The inner function stores a
reference to that same cell in its __closure__. So double and triple
each close over a different cell, which is why they remember different
factors. (This also explains the classic "late binding" gotcha: closures
capture the cell, not the value at definition time.)
map and filter are lazy iterator objects written in C. map(f, xs)
doesn't call f at all yet — it returns a map object that calls f(next(xs))
once each time you call next() on it. list(map(...)) is what forces the
work. A comprehension does the same work but in Python bytecode with an
inlined loop, which is why it's often marginally faster and always more
readable for simple cases.
A decorator is just a function call performed at def time. @timed
above def slow_square compiles to: define the function, then immediately
rebind the name to timed(slow_square). functools.reduce is a plain C loop
that threads an accumulator through the sequence — nothing magic, just
acc = f(acc, item) repeated.
Exercise¶
Given a list of order dictionaries like
{"item": "Book", "price": 12.5, "qty": 2}, use map and a lambda to compute
each order's total (price * qty), filter to keep only orders over $20, and
functools.reduce to sum the grand total of the filtered orders — then write
the same pipeline again using comprehensions and sum() and compare
readability.