The Complete Guide to Python's map() & lambda
From anonymous functions and functional patterns to 2D matrices, 3D tensors, ML operations and beyond — one unified technical reference for applying mathematics in pure Python.
Lambda Functions — Complete Reference
Functional Purity & Mathematics: The concept of lambda originates from Lambda Calculus, developed by Alonzo Church. In functional programming, lambdas are preferred because they discourage state mutations and side effects, making algorithms completely deterministic and mathematically provable.
A lambda is Python's syntax for an anonymous function — a function object created
inline without a def statement. It originates from the mathematical concept of lambda
calculus introduced by Alonzo Church in the 1930s, the same theoretical foundation underlying all of
functional programming.
Core Syntax
The body is limited to a single expression — no statements, no loops, no assignments. It returns the result of that expression implicitly.
File: lambda_anatomy.py
# ─── Anatomy of a lambda ─────────────────────────────────────── # Zero arguments greet = lambda: "Moien!" # → "Moien!" # Single argument square = lambda x: x ** 2 # → 9 (when x=3) # Multiple arguments dot = lambda a, b: sum(x*y for x,y in zip(a,b)) # dot product # Default argument scale = lambda x, k=2: x * k # → 6 (when x=3) # *args — variadic positional vec_sum = lambda *args: sum(args) # → 15 (1+2+3+4+5) # **kwargs — variadic keyword describe = lambda **kw: ', '.join(f'{k}={v}' for k,v in kw.items()) print(describe(name='Arman', city='Luxembourg')) # → name=Arman, city=Luxembourg # Mixed: positional + *args + **kwargs mixed = lambda x, *args, **kw: (x, args, kw) print(mixed(1, 2, 3, y=4)) # → (1, (2, 3), {'y': 4}) # Ternary conditional inside lambda relu = lambda x: x if x > 0 else 0 # ReLU activation # Immediately Invoked Lambda Expression (IILE) result = (lambda x, y: x + y)(3, 4) # → 7 # Lambda returning a lambda (currying) multiply_by = lambda k: (lambda x: x * k) double = multiply_by(2) triple = multiply_by(3) print(double(10)) # → 20 print(triple(10)) # → 30
Lambda as a First-Class Object
In Python, functions are first-class citizens. A lambda can be stored in a variable, in a list or dictionary, passed as an argument, or returned from another function.
File: lambda_first_class.py
# ─── Lambda stored in data structures ────────────────────────── # Dictionary of operations (dispatch table) ops = { "add" : lambda a, b: a + b, "sub" : lambda a, b: a - b, "mul" : lambda a, b: a * b, "dot" : lambda u, v: sum(x*y for x,y in zip(u,v)), "config": lambda **kw: {'defaults': {'lr':0.001}, **kw}, } ops["dot"]([1,2,3], [4,5,6]) # → 32 (1×4 + 2×5 + 3×6) ops["config"](lr=0.01, epochs=10) # → {'defaults':..., 'lr':0.01, ...} # List of activation functions (ML pattern) activations = [ lambda x: x, # identity lambda x: x if x > 0 else 0, # relu lambda x: 1 / (1 + 2.718**(-x)), # sigmoid ≈ lambda x: (2.718**x - 2.718**(-x)) / (2.718**x + 2.718**(-x)), # tanh ≈ ] print(activations[1](-3)) # → 0 (relu) print(activations[1](7)) # → 7
Closures & Scope (LEGB)
A lambda closes over variables from its enclosing scope. This is powerful for creating parameterised functions on the fly — critical in linear algebra where you want to generate scaled or shifted transforms.
File: lambda_closure.py
# ─── Closure: lambda captures the outer scope ────────────────── def make_scaler(k): """Returns a function that multiplies a vector by scalar k.""" return lambda vec: [k * x for x in vec] scale_by_3 = make_scaler(3) scale_by_π = make_scaler(3.14159) print(scale_by_3([1, 2, 3])) # → [3, 6, 9] print(scale_by_π([1, 0, 0])) # → [3.14159, 0.0, 0.0] # ─── Classic late-binding trap ───────────────────────────────── # WRONG: all lambdas capture the same `i` at loop end bad = [lambda x: x * i for i in range(3)] good = [lambda x, i=i: x * i for i in range(3)] # fix: capture now print([f(5) for f in bad]) # → [10, 10, 10] ← wrong print([f(5) for f in good]) # → [0, 5, 10] ← correct
Late Binding Trap: Lambdas inside loops don't capture the value of the loop variable — they
capture the variable itself. Always use a default argument i=i to snapshot the current value.
Sorting with lambda
One of the most practical uses of lambda is as the key argument to sorted(). Return a
tuple to sort by multiple criteria — Python compares tuples lexicographically, breaking ties on
subsequent elements.
File: lambda_sorting.py
import math vectors = [[3,4], [1,1], [5,12], [0,7]] # Sort by Euclidean magnitude (L2 norm) by_magnitude = sorted(vectors, key=lambda v: math.sqrt(sum(x**2 for x in v))) # → [[1,1], [0,7], [3,4], [5,12]] # ─── Multi-key tuple sort ─────────────────────────────────────── # Primary: GPA descending (-s[1]) Secondary: name ascending (s[0]) students = [('Bob',3.5), ('Alice',3.5), ('Carol',3.9), ('Dave',3.9)] ranked = sorted(students, key=lambda s: (-s[1], s[0])) # → [('Carol',3.9), ('Dave',3.9), ('Alice',3.5), ('Bob',3.5)] # Applied to matrix rows: sort by row sum desc, then first element asc M = [[3,1,4], [1,5,9], [2,6,5], [1,4,9]] sorted_M = sorted(M, key=lambda row: (-sum(row), row[0])) # → [[1,5,9], [1,4,9], [2,6,5], [3,1,4]] # Multi-key: length first, then first element data = [("v2",[1,2]), ("v3",[1,2,3]), ("v1",[3])] sorted(data, key=lambda t: (len(t[1]), t[1][0])) # → [('v1',[3]), ('v2',[1,2]), ('v3',[1,2,3])]
map() — Complete Reference
The Functor Pattern: In category theory, a functor is a mapping between categories.
In Python, map() acts as a functor by safely applying a transformation (your lambda) to every
element inside a container without altering the structure of the container itself.
map(function, iterable, ...) applies function to every element of one or more
iterables, returning a lazy iterator. It embodies the functor pattern: a structure-preserving
transformation over a container.
Lazy Evaluation: map() returns a map object (an iterator), not a list.
It computes values on demand. Wrap with list(), tuple(), or iterate with a loop to
materialise results — memory-efficient for large matrices.
Basic Usage Patterns
File: map_basics.py
v = [1, 2, 3, 4, 5] # 1. With a named function def square(x): return x ** 2 list(map(square, v)) # → [1, 4, 9, 16, 25] # 2. With a lambda (most common) list(map(lambda x: x**2, v)) # → [1, 4, 9, 16, 25] # 3. With a built-in function list(map(str, v)) # → ['1','2','3','4','5'] list(map(abs, [-3, 1, -4])) # → [3, 1, 4] # 4. Multiple iterables — function accepts n args u = [1, 2, 3]; w = [4, 5, 6] list(map(lambda a, b: a+b, u, w)) # → [5, 7, 9] (vector addition) list(map(lambda a, b: a*b, u, w)) # → [4, 10, 18] (Hadamard product) # 5. Chained maps (function composition) result = list(map(lambda x: x*2, # step 2: scale map(lambda x: x+1, # step 1: shift v))) # → [4, 6, 8, 10, 12] i.e., (v+1)*2 # 6. map is lazy — materialise when needed m = map(lambda x: x**3, v) # no computation yet next(m) # → 1 (computes only first) list(m) # → [8, 27, 64, 125] (rest) # 7. Stopping at shortest iterable list(map(lambda a, b: a*b, [1,2,3], [10,20])) # → [10, 40] — stops at length of shortest
String Operations with map()
Python's built-in string methods are function objects — they can be passed directly to map()
without wrapping in a lambda. This makes data-cleaning pipelines concise and readable.
File: map_strings.py
words = [' hello ', ' world ', ' python '] # str.strip, str.upper are unbound methods → pass directly stripped = list(map(str.strip, words)) # → ['hello', 'world', 'python'] upper = list(map(str.upper, stripped)) # → ['HELLO', 'WORLD', 'PYTHON'] lengths = list(map(len, stripped)) # → [5, 5, 6] # Type conversion str_nums = ['1', '2', '3', '4', '5'] ints = list(map(int, str_nums)) # → [1, 2, 3, 4, 5] floats = list(map(float, str_nums)) # → [1.0, 2.0, 3.0, 4.0, 5.0] # Pipeline: strip + lower in one chain result = list(map(str.strip, map(str.lower, [' HI ', ' BYE ']))) # → ['hi', 'bye'] # Data-cleaning: strip + split each CSV row into a matrix raw_rows = [' 1,2,3 ', ' 4,5,6 '] matrix = list(map( lambda r: list(map(int, r.strip().split(','))), raw_rows )) # → [[1, 2, 3], [4, 5, 6]] — 2D matrix from CSV strings
map() vs filter() vs reduce()
map(f, iter)
- Transforms every element
- Output length = Input length
- Returns an iterator
- Example:
map(lambda x: x**2, v)
filter(pred, iter)
- Selects elements where pred is True
- Output length ≤ Input length
- Returns an iterator
- Example:
filter(lambda x: x>0, v)
File: map_filter_reduce.py
from functools import reduce v = [-3, -1, 0, 2, 4] # map → transform all list(map(lambda x: x**2, v)) # → [9, 1, 0, 4, 16] # filter → keep where condition is True list(filter(lambda x: x > 0, v)) # → [2, 4] # reduce → fold/accumulate to a single value reduce(lambda acc, x: acc + x, v) # → 2 (sum) reduce(lambda acc, x: acc * x, v) # → 0 (product) # Chained pipeline: square positives then sum reduce(lambda a, b: a+b, map(lambda x: x**2, filter(lambda x: x > 0, v))) # → 20 (4 + 16)
lambda vs map vs List Comprehension
These three tools often solve the same problem. Knowing which to choose requires understanding their trade-offs in readability, performance, and expressiveness.
| Feature | lambda | map() | List Comprehension |
|---|---|---|---|
| Returns | Function object | Iterator | List (eager) |
| Memory | O(1) | O(1) — lazy | O(n) — eager |
| Filtering | No | No (use filter) | Yes — if clause |
| Multiple iterables | Yes (args) | Yes (zip natively) | Yes (zip()) |
| Nested loops | Awkward | Possible (nested map) | Clean |
| Reusable | Yes (assigned) | No | No |
| Debuggable | Hard — no name | Hard — opaque | Easier |
| PEP8 preference | Sparingly | When functional style | Preferred for simple cases |
| Performance (CPython) | — | ~same as LC | Slightly faster than map+lambda |
File: equivalence.py
v = [1, 2, 3, 4] # 1. for loop (imperative) r1 = [] for x in v: r1.append(x**2) # 2. list comprehension (Pythonic, readable) r2 = [x**2 for x in v] # 3. map + lambda (functional) r3 = list(map(lambda x: x**2, v)) # 4. map + named function (most readable for complex ops) def sq(x): return x**2 r4 = list(map(sq, v)) # All produce → [1, 4, 9, 16] # ─── When map shines: multiple iterables ─────────────────────── u = [1, 2, 3]; w = [4, 5, 6] lc = [a+b for a, b in zip(u, w)] # list comp needs zip mp = list(map(lambda a,b:a+b, u, w)) # map takes both natively # Both → [5, 7, 9] # ─── When map shines: lazy streaming ─────────────────────────── import itertools stream = map(lambda x: x**2, itertools.count(1)) next(stream) # → 1 next(stream) # → 4 (infinite sequence, zero memory overhead)
Rule of Thumb: Use a list comprehension for simple, readable transforms. Use
map() when: (1) you already have a named function, (2) piping results into another iterator
without materialising, or (3) applying to multiple iterables natively. Use lambda when the
function is short and disposable.
Linear Algebra Foundations & Complete Norm Reference
Before connecting map and lambda to matrix operations, we need a precise mental model
of data structures and the mathematical objects they represent.
Hierarchy of Mathematical Objects in Python
Scalars & Vectors
- Scalar:
x = 3.14— a single number, rank-0 tensor - Vector:
[1, 2, 3]— 1D list, rank-1 tensor - Column vs Row: convention — list = row vector
- Dot product:
sum(a*b for a,b in zip(u,v))
Matrices & Tensors
- 2D matrix: list of lists
[[row0], [row1]] - 3D tensor: list of 2D matrices (batch/depth)
- Shape:
(rows, cols)or(depth, rows, cols) - Element:
M[i][j]for row i, col j
Mathematical Correspondence:
Scalar multiplication: k·v = [k·v₀, k·v₁, ..., k·vₙ] → map(lambda x: k*x, v)
Vector addition: u + v = [u₀+v₀, u₁+v₁, ...] → map(lambda a,b: a+b, u, v)
Matrix scalar op: k·A → map(lambda row: map(lambda x: k*x, row), A)
Function application: f(A) → map(lambda row: map(f, row), A)
Core Building Blocks
File: la_building_blocks.py
# ─── Core vector operations via map + lambda ─────────────────── v_scale = lambda v, k: list(map(lambda x: k*x, v)) v_add = lambda u, v: list(map(lambda a,b: a+b, u, v)) v_dot = lambda u, v: sum(map(lambda a,b: a*b, u, v)) v_norm = lambda v: v_dot(v,v)**0.5 v_unit = lambda v: v_scale(v, 1/v_norm(v)) u = [3, 4] print(v_norm(u)) # → 5.0 print(v_unit(u)) # → [0.6, 0.8] print(v_dot([1,0],[0,1])) # → 0 (perpendicular) # Utility: materialise a nested map result into a list-of-lists mat = lambda m: [list(row) for row in m] shape = lambda m: (len(m), len(m[0]))
Complete Norm Reference (L1, L2, Lp, L∞)
Norm Family:
L1 (Manhattan): ‖v‖₁ = Σ|vᵢ|
L2 (Euclidean): ‖v‖₂ = √(Σvᵢ²)
Lp (General): ‖v‖ₚ = (Σ|vᵢ|ᵖ)^(1/p)
L∞ (Chebyshev): ‖v‖∞ = max(|vᵢ|) — the limiting case of Lp as p→∞
File: norms_complete.py
import math # ─── Complete norm suite ──────────────────────────────────────── norm_l1 = lambda v: sum(map(abs, v)) norm_l2 = lambda v: math.sqrt(sum(map(lambda x: x**2, v))) norm_lp = lambda v, p: sum(map(lambda x: abs(x)**p, v)) ** (1/p) norm_inf = lambda v: max(map(abs, v)) normalize = lambda v: list(map(lambda x: x / norm_l2(v), v)) vec = [3, 4, 0] print(norm_l1(vec)) # → 7.0 (3+4+0) print(norm_l2(vec)) # → 5.0 (√(9+16+0)) print(norm_lp(vec, 3)) # → 4.498 (∛(27+64+0)) print(norm_lp(vec, 100)) # → ≈4.0 (approaches L∞ as p→∞) print(norm_inf(vec)) # → 4 (max of |3|,|4|,|0|) print(normalize([3,4])) # → [0.6, 0.8] # ─── Angle between two vectors ───────────────────────────────── cos_sim = lambda a, b: v_dot(a,b) / (norm_l2(a) * norm_l2(b) + 1e-10) # Clamp before acos: float rounding can push cos slightly outside [-1,1] angle_deg = lambda a, b: math.degrees(math.acos(max(-1, min(1, cos_sim(a,b))))) print(ff'Angle: {angle_deg([1,2,3],[4,5,6]):.2f}°') # → Angle: 12.93° print(angle_deg([1,0], [0,1])) # → 90.0° (perpendicular) print(angle_deg([1,0], [1,0])) # → 0.0° (same direction)
2D Matrix Operations
A 2D matrix is a list of lists: M[i][j] is the element at row i, column j. We
can express every fundamental matrix operation using nested map and lambda.
Scalar Multiplication k·A
M=[[1,2],[3,4]]; k=3 kA = list(map( lambda row: list(map( lambda x: k*x, row)), M)) # → [[3,6],[9,12]]
Matrix Addition A + B
A=[[1,2],[3,4]] B=[[5,6],[7,8]] C = list(map( lambda r1,r2: list(map( lambda a,b: a+b, r1, r2)), A, B)) # → [[6,8],[10,12]]
Transpose Aᵀ
A=[[1,2,3],[4,5,6]] T = list(map( lambda col: list(col), zip(*A))) # → [[1,4],[2,5],[3,6]] # zip(*A) unpacks rows into cols
Hadamard Product A⊙B
A=[[1,2],[3,4]] B=[[2,0],[1,3]] H = list(map( lambda r1,r2: list(map( lambda a,b: a*b, r1, r2)), A, B)) # → [[2,0],[3,12]]
Apply f Element-wise
M = [[1,-2],[3,-4]] apply = lambda f, M: list(map( lambda row: list(map(f, row)), M)) apply(abs, M) # → [[1,2],[3,4]] apply(lambda x:-x, M) # → [[-1,2],[-3,4]]
Row & Column Sums
M=[[1,2,3],[4,5,6]] row_sums = list(map(sum, M)) # → [6, 15] col_sums = list(map( sum, zip(*M))) # → [5, 7, 9]
Matrix–Vector Multiplication
File: matvec.py
dot = lambda row, v: sum(map(lambda a, b: a*b, row, v)) matvec = lambda M, v: list(map(lambda row: dot(row, v), M)) A = [[1,2,3],[4,5,6],[7,8,9]] print(matvec(A, [1, 0, -1])) # → [-2, -2, -2] # Identity check I = [[1,0,0],[0,1,0],[0,0,1]] print(matvec(I, [3,7,2])) # → [3, 7, 2] (unchanged) # Projection onto unit vector n: proj_n(v) = (v·n)·n n = [1, 0, 0] # unit x-axis proj = lambda v, n: list(map(lambda x: dot(v,n)*x, n)) print(proj([3,4,5], n)) # → [3, 0, 0] (x-component only) # Batch: apply matvec to multiple vectors results = list(map(lambda v: matvec(A, v), [[1,0,0],[0,1,0],[1,1,1]]))
Outer Product u ⊗ v
The outer product expands two vectors into a matrix — the complement of the dot product. Each row i of the result is u[i] × v.
File: outer_product.py
outer = lambda u, v: list(map( lambda x: list(map(lambda y: x * y, v)), u)) print(outer([1,2,3], [4,5,6])) # → [[ 4, 5, 6], ← 1 × v # [ 8, 10, 12], ← 2 × v # [12, 15, 18]] ← 3 × v # dot(u,v) → scalar (m,) × (m,) → 1 # outer(u,v) → matrix (m,) × (n,) → (m,n) # Outer products appear in SVD: A ≈ Σᵢ σᵢ (uᵢ ⊗ vᵢ)
Matrix Multiplication
File: matmul.py
def matmul(A, B): """C = A @ B — pure map/lambda implementation.""" Bt = list(map(list, zip(*B))) # transpose B for col access dot = lambda r, c: sum(map(lambda a,b: a*b, r, c)) return list(map( lambda row: list(map(lambda col: dot(row, col), Bt)), A)) # 2×3 × 3×2 = 2×2 A = [[1,2,3],[4,5,6]] B = [[7,8],[9,10],[11,12]] matmul(A, B) # → [[58, 64], [139, 154]] # A[0]·B_col0 = 1×7 + 2×9 + 3×11 = 58 ✓ # Compact one-liner matmul2 = lambda A, B: \ [[sum(a*b for a,b in zip(row,col)) for col in zip(*B)] for row in A]
Determinant, Trace & Frobenius Norm
File: det_trace.py
# ─── Trace: sum of diagonal elements ─────────────────────────── trace = lambda M: sum(map(lambda i: M[i][i], range(len(M)))) diag = lambda M: list(map(lambda i: M[i][i], range(min(len(M), len(M[0]))))) M = [[1,2,3],[4,5,6],[7,8,9]] print(trace(M)) # → 15 (1 + 5 + 9) print(diag(M)) # → [1, 5, 9] # ─── Frobenius norm: sqrt(sum of squared elements) ───────────── frob = lambda M: sum( map(lambda row: sum(map(lambda x: x**2, row)), M))**0.5 print(frob([[1,2],[3,4]])) # → √30 ≈ 5.477 # ─── Recursive n×n Determinant (Laplace cofactor expansion) ──── # minor: delete row i and column j minor = lambda M, i, j: [ row[:j] + row[j+1:] for row in (M[:i] + M[i+1:]) ] def det(M): """Determinant of n×n matrix — Laplace expansion, O(n!).""" if len(M) == 1: return M[0][0] return sum(map( lambda j: ((-1)**j) * M[0][j] * det(minor(M, 0, j)), range(len(M[0])) )) print(det([[3,8],[4,6]])) # → -14 print(det([[1,2,3],[4,5,6],[7,8,10]])) # → -3 print(det([[1,2,3],[4,5,6],[7,8,9]])) # → 0 (singular)
Complexity: Laplace expansion is O(n!) — never use it for matrices larger than ~8×8. In
production, use numpy.linalg.det() which runs O(n³) via LU decomposition.
Row Operations (Gaussian Elimination Building Blocks)
File: row_ops.py
# Scale row i by scalar k def scale_row(M, i, k): return list(map( lambda j_row: list(map(lambda x: k*x, j_row[1])) if j_row[0] == i else j_row[1], enumerate(M))) # Add k×(row src) to row dst def add_scaled_row(M, dst, src, k): src_row = M[src] return list(map( lambda j_row: list(map(lambda a,b: a+k*b, j_row[1], src_row)) if j_row[0] == dst else j_row[1], enumerate(M))) A = [[2,1,-1],[-3,-1,2],[-2,1,2]] A2 = add_scaled_row(A, 1, 0, 1.5) # row1 += 1.5 × row0 A3 = add_scaled_row(A2, 2, 0, 1.0) # row2 += 1.0 × row0
3D Tensor Operations
A 3D tensor is a list of 2D matrices: T[d][i][j] where d is the depth/batch index,
i the row, j the column. This is the natural shape for batched ML data (batch_size × rows ×
cols) or volumetric data (depth × height × width).
Notation: A 3D tensor of shape (D, R, C) has D slices, each an R×C matrix. Accessing
element: T[d][r][c]. In NumPy this is T[d, r, c].
File: tensor_3d.py
# Shape (D, R, C) = (3, 2, 4) T = [ [[1, 2, 3, 4], [5, 6, 7, 8]], # slice 0 [[9,10,11,12], [13,14,15,16]], # slice 1 [[17,18,19,20], [21,22,23,24]], # slice 2 ] shape3 = lambda T: (len(T), len(T[0]), len(T[0][0])) print(shape3(T)) # → (3, 2, 4) # ─── Scalar multiply ──────────────────────────────────────────── t_scale = lambda T, k: list(map( lambda mat: list(map( lambda row: list(map(lambda x: k*x, row)), mat)), T)) # ─── Element-wise addition of two 3D tensors ─────────────────── t_add = lambda A, B: list(map( lambda m1, m2: list(map( lambda r1, r2: list(map(lambda a,b: a+b, r1, r2)), m1, m2)), A, B)) # ─── Apply function to every element ─────────────────────────── t_apply = lambda f, T: list(map( lambda mat: list(map( lambda row: list(map(f, row)), mat)), T)) # Apply ReLU to entire tensor (after negation) relu = lambda x: x if x > 0 else 0 T_relu = t_apply(relu, t_scale(T, -1)) # negate then relu → all zeros # ─── Get all elements at position [i][j] across depth ────────── depth_col = lambda T, i, j: list(map(lambda mat: mat[i][j], T)) print(depth_col(T, 0, 0)) # → [1, 9, 17]
Full Axis-Swap Transpose & Normalisation
The full axis-swap changes the tensor's fundamental shape: (D, R, C) → (C, R, D) — analogous to
NumPy's np.transpose(T, (2,1,0)). This differs from transposing each 2D slice independently.
File: tensor_axis_and_norm.py
# ─── Layer-wise transpose: (D, R, C) → (D, C, R) ─────────────── transpose3d_layerwise = lambda T: list(map( lambda layer: list(map(list, zip(*layer))), T)) # ─── Full axis-swap: (D, R, C) → (C, R, D) ──────────────────── # new_tensor[c][r][d] = T[d][r][c] swap_axes = lambda T: list(map( lambda c: list(map( lambda r: list(map( lambda d: T[d][r][c], range(len(T)))), # depth axis range(len(T[0])))), # row axis range(len(T[0][0])))) # col axis (becomes new depth) # ─── Normalise every element by global max ───────────────────── T2 = [[[1,2,3],[4,5,6]], [[7,8,9],[10,11,12]]] global_max = max(map( lambda layer: max(map(lambda row: max(row), layer)), T2)) # → 12 normalised = list(map( lambda layer: list(map( lambda row: list(map( lambda x: round(x/global_max, 3), row)), layer)), T2)) # → [[[0.083,0.167,0.25],[0.333,0.417,0.5]], # [[0.583,0.667,0.75],[0.833,0.917,1.0]]] # ─── Flatten 3D + reshape ─────────────────────────────────────── from functools import reduce flatten3 = lambda T: reduce( lambda acc, mat: acc + reduce(lambda a,r: a+r, mat, []), T, []) depth_sum = lambda T: reduce( lambda acc, mat: list(map(lambda r1,r2: list(map(lambda a,b: a+b, r1, r2)), acc, mat)), T)
Batched Matrix Multiplication
File: batched_matmul.py
# ─── Batched matmul: T(B,M,K) @ W(K,N) → out(B,M,N) ─────────── def batch_matmul(T, W): """Apply matrix W to each 2D slice in tensor T.""" return list(map(lambda mat: matmul(mat, W), T)) T = [ [[1,0,0],[0,1,0]], [[1,2,3],[4,5,6]], [[7,8,9],[1,1,1]], ] W = [[1,2],[3,4],[5,6]] out = batch_matmul(T, W) # out[0] = [[1,2],[3,4]] (identity slice) # out[1] = [[22,28],[49,64]] # out[2] = [[76,100],[9,12]]
Advanced: ML Operations, Activations & Attention
Modern deep learning is built on element-wise nonlinear functions applied to matrices and tensors. Every
activation function is a perfect map + lambda expression.
File: activation_functions.py
import math # ─── Scalar activations (lambdas) ─────────────────────────────── relu = lambda x: x if x > 0 else 0 leaky = lambda x, α=0.01: x if x > 0 else α*x sigmoid = lambda x: 1 / (1 + math.exp(-x)) tanh_fn = lambda x: math.tanh(x) gelu = lambda x: x * 0.5 * (1 + math.erf(x / math.sqrt(2))) swish = lambda x: x * sigmoid(x) # ─── Apply to vector or 2D matrix ────────────────────────────── apply_v = lambda f, v: list(map(f, v)) apply_m = lambda f, M: list(map(lambda row: list(map(f, row)), M)) z = [[-2,-1,0,1,2]] print(apply_m(relu, z)) # → [[0, 0, 0, 1, 2]] print(apply_m(sigmoid, z)) # → [[0.12, 0.27, 0.50, 0.73, 0.88]] print(apply_m(tanh_fn, z)) # → [[-0.96, -0.76, 0, 0.76, 0.96]]
Softmax & Layer Normalisation
File: softmax_layernorm.py
import math # ─── Numerically stable softmax ──────────────────────────────── def softmax(v): m = max(v) # stability: subtract max exps = list(map(lambda x: math.exp(x-m), v)) s = sum(exps) return list(map(lambda e: e/s, exps)) # Batch softmax: row-wise over a matrix batch_softmax = lambda M: list(map(softmax, M)) logits = [[1.0,2.0,3.0],[0.5,0.5,0.5],[10,0,-10]] probs = batch_softmax(logits) # → [[0.090,0.245,0.665], [0.333,0.333,0.333], [1.000,0.000,0.000]] # ─── Layer Normalisation (simplified) ────────────────────────── def layer_norm(v, eps=1e-8): mean = sum(v) / len(v) var = sum(map(lambda x: (x-mean)**2, v)) / len(v) std = (var + eps)**0.5 return list(map(lambda x: (x-mean)/std, v)) layer_norm([2,4,4,4,5,5,7,9]) # → [-1.5, -0.5, -0.5, -0.5, 0.0, 0.0, 1.0, 2.0]
Linear Layer Forward Pass Y = f(XW + b)
File: linear_layer.py
import math def linear(X, W, b, activation=lambda x: x): # X: (batch, in) W: (in, out) b: (out,) XW = matmul(X, W) # (batch, out) return list(map( lambda row: list(map( lambda z, bi: activation(z+bi), row, b)), XW)) relu = lambda x: max(0, x) sigmoid = lambda x: 1/(1+math.exp(-x)) # Input: batch of 2, each with 3 features X = [[0.5,1.0,0.2],[0.1,0.8,0.9]] # Layer 1: (3 → 4), ReLU W1 = [[0.1,0.2,-0.3,0.4],[0.5,-0.1,0.2,0.3],[-0.2,0.4,0.1,-0.1]] b1 = [0.1,0.0,-0.1,0.05] H1 = linear(X, W1, b1, relu) # (2, 4) # Layer 2: (4 → 1), Sigmoid (binary classification) W2 = [[0.6],[-0.3],[0.5],[0.2]] Y = linear(H1, W2, [0.0], sigmoid) # (2, 1)
Scaled Dot-Product Attention
File: attention.py
import math def attention(Q, K, V): """Scaled dot-product attention. Q, K, V are (seq_len × d_k) matrices.""" d_k = len(Q[0]); scale = d_k**0.5 Kt = list(map(list, zip(*K))) d = lambda r, c: sum(map(lambda a,b: a*b, r, c)) scores= [[d(q,k)/scale for k in Kt] for q in Q] sm = lambda v: ( lambda e: list(map(lambda x: x/sum(e), e)) )(list(map(lambda x: math.exp(x-max(v)), v))) attn = list(map(sm, scores)) Vt = list(map(list, zip(*V))) return [[d(a,v) for v in Vt] for a in attn]
Practical Applications
Four real-world applications that make map & lambda concrete: image pixel processing, gradient
descent, cosine similarity matrices, and composing linear transforms.
Image Processing — Pixel Manipulation
A colour image is a 3D tensor of shape (H × W × 3). Each pixel is an RGB tuple. map + lambda
applies transformations channel-by-channel or pixel-by-pixel across the entire image.
File: image_processing.py
# ─── Simulate 2×2 RGB image ───────────────────────────────────── image = [ [(255,128,0), (0,255,128)], [(64,64,64), (200,100,50)], ] # 1. Grayscale: 0.299R + 0.587G + 0.114B to_gray = lambda px: int(0.299*px[0] + 0.587*px[1] + 0.114*px[2]) gray_img = list(map(lambda row: list(map(to_gray, row)), image)) # → [[145, 212], [64, 170]] # 2. Invert colours: 255 - channel invert = lambda px: tuple(map(lambda c: 255-c, px)) inv_img = list(map(lambda row: list(map(invert, row)), image)) # 3. Brightness adjustment (clamped to [0, 255]) brighten = lambda factor: ( lambda px: tuple(map(lambda c: min(255, int(c*factor)), px))) bright_img = list(map(lambda row: list(map(brighten(1.5), row)), image)) # 4. Channel extraction (R/G/B plane) channel = lambda img, ch: list(map(lambda row: list(map(lambda px: px[ch], row)), img)) red_plane = channel(image, 0) # → [[255, 0], [64, 200]]
Gradient Descent Step
File: gradient_descent.py
from functools import reduce # θ_new = θ - α * ∇L grad_step = lambda θ, g, α: list(map(lambda p,d: p-α*d, θ, g)) params = [0.5,-0.3,1.2,0.8] grads = [0.1, 0.2,-0.05,0.15] lr = 0.01 print(grad_step(params, grads, lr)) # → [0.499, -0.302, 1.2005, 0.7985] # ─── Multi-step training loop via reduce ──────────────────────── # reduce(f, [s1,s2,...,s100], init) ≡ f(f(f(init, s1), s2), ..., s100) steps = [(grads, lr)] * 100 final = reduce( lambda θ, step: grad_step(θ, step[0], step[1]), steps, params) # initial value = params print([round(x,4) for x in final]) # → [0.4, -0.5, 1.25, 0.65]
Full Cosine Similarity Matrix (n×n)
File: cosine_matrix.py
import math dot = lambda a, b: sum(map(lambda x,y: x*y, a, b)) norm = lambda v: math.sqrt(sum(map(lambda x: x**2, v))) cosine = lambda a, b: dot(a,b)/(norm(a)*norm(b)) if norm(a)*norm(b) else 0 vectors = [[1,0,0], [0,1,0], [1,1,0], [1,1,1]] sim_matrix = list(map( lambda u: list(map(lambda v: round(cosine(u,v),4), vectors)), vectors)) for row in sim_matrix: print(row) # [1.0, 0.0, 0.7071, 0.5774] # [0.0, 1.0, 0.7071, 0.5774] # [0.7071, 0.7071, 1.0, 0.8165] # [0.5774, 0.5774, 0.8165, 1.0 ] # Diagonal = 1.0 (self-similarity) Matrix is symmetric
Linear Transformation Composition
Multiple linear transforms can be composed into a single matrix using reduce + matmul.
The composed matrix applies all transforms in one shot, eliminating repeated multiplications at inference time.
File: transform_composition.py
from functools import reduce # 2D linear transforms T_scale = [[2,0],[0,2]] # scale by 2 T_rot90 = [[0,-1],[1,0]] # rotate 90° CCW T_shear = [[1,1],[0,1]] # shear in x-direction # Compose: ((T_scale @ T_rot90) @ T_shear) composed = reduce(matmul, [T_scale, T_rot90, T_shear]) # → [[-2,-1],[2,0]] # Apply to a batch of points — one map call instead of 3× matvec matvec = lambda M, v: list(map(lambda row: dot(row,v), M)) points = [[1,0],[0,1],[1,1],[2,3]] mapped_pts = list(map(lambda p: matvec(composed, p), points)) # All four points transformed in one map call
NumPy Integration & Pipelines
Even in NumPy-heavy code, map and lambda serve as the bridge for custom row-wise
operations and composable preprocessing pipelines. Below: equivalents table and advanced patterns.
NumPy Equivalents Reference
File: numpy_bridge.py
import numpy as np A=[[1,2],[3,4]]; An=np.array(A) B=[[5,6],[7,8]]; Bn=np.array(B) # Scalar multiply list(map(lambda r:list(map(lambda x:x*3,r)),A)) # ← map/lambda An * 3 # ← numpy # Matrix add list(map(lambda r1,r2:list(map(lambda a,b:a+b,r1,r2)),A,B)) An + Bn # Transpose list(map(list, zip(*A))) # ← map/lambda An.T # ← numpy # Matrix multiply matmul(A, B) # ← our implementation An @ Bn # ← numpy # Element-wise ReLU t_apply(lambda x:max(0,x),A) # ← map/lambda np.maximum(0, An) # ← numpy # Row sums list(map(sum, A)) # ← map/lambda An.sum(axis=1) # ← numpy # Frobenius norm frob(A) # ← our lambda np.linalg.norm(An, 'fro') # ← numpy
NumPy + map/lambda Patterns
File: numpy_map_patterns.py
import numpy as np A = np.array([[1,2,3],[4,5,6],[7,8,9]], dtype=float) # Row-wise L2 norm row_norms = list(map(lambda row: np.linalg.norm(row), A)) # → [3.742, 8.775, 13.928] # Normalise each row to unit length norm_rows = list(map(lambda row: row / np.linalg.norm(row), A)) # Row-wise softmax using NumPy softmax_np = lambda x: (lambda e: e/e.sum())(np.exp(x-x.max())) scores = np.array([[2.0,1.0,0.1],[1.0,3.0,0.2]]) probs = list(map(softmax_np, scores)) # Batch normalisation row-by-row batch_norm = lambda M: np.array(list(map( lambda row: (row-row.mean())/(row.std()+1e-8), M)))
Lambda Pipeline with reduce
Define a list of lambdas representing processing stages, then fold them sequentially using reduce.
This makes it easy to add, remove, or reorder preprocessing steps without rewriting nested calls.
File: numpy_pipeline.py
import numpy as np from functools import reduce # ─── Define pipeline as a list of lambdas ────────────────────── pipeline = [ lambda M: M - M.mean(axis=0), # 1. centre columns lambda M: M / (M.std(axis=0) + 1e-8), # 2. scale columns lambda M: np.clip(M, -3, 3), # 3. clip outliers lambda M: np.round(M, 4), # 4. round for display ] # ─── Run with reduce: each step's output feeds the next ──────── apply_pipeline = lambda data, steps: reduce(lambda M,f: f(M), steps, data) raw = np.array([[1.0,200.0],[2.0,400.0],[3.0,600.0],[4.0,800.0]]) result = apply_pipeline(raw, pipeline) # Extend at runtime — just append pipeline.append(lambda M: M * 100) # 5. scale to percentages result2 = apply_pipeline(raw, pipeline)
Production Note: For real matrix operations in data engineering or ML, always use NumPy,
PyTorch, or TensorFlow. Pure Python map/lambda is for learning, scripting, and
small-scale transforms — NumPy vectorisation is 10–100× faster because it operates on contiguous C arrays.
Performance, Pitfalls & Best Practices
Performance Characteristics & timeit Benchmark
| Approach | Typical Time (1M elements, 5 runs) | Relative Speed | Memory |
|---|---|---|---|
map + lambda |
~1.20s | 1× (baseline) | O(1) lazy |
list comprehension |
~0.90s | 1.3× faster | O(n) eager |
named fn + map |
~0.80s | 1.5× faster | O(1) lazy |
NumPy arr**2 |
~0.02s | 60× faster | Contiguous C array |
File: benchmark.py
import timeit; import numpy as np N = 1_000_000; data = list(range(N)); arr = np.array(data) t1 = timeit.timeit(lambda: list(map(lambda x: x**2, data)), number=5) t2 = timeit.timeit(lambda: [x**2 for x in data], number=5) def sq(x): return x**2 t3 = timeit.timeit(lambda: list(map(sq, data)), number=5) t4 = timeit.timeit(lambda: arr**2, number=5) print(f"map + lambda : {t1:.3f}s") print(f"list comprehension: {t2:.3f}s") print(f"named fn + map : {t3:.3f}s") print(f"NumPy vectorised : {t4:.4f}s") # Key insight: lambda adds overhead vs named function. # Replacing lambda x: x**2 with def sq(x) gives measurable gain. # Neither approaches NumPy for numerical work.
Key Pitfalls to Avoid
File: pitfalls.py
# ─── Pitfall 1: Consuming an iterator twice ───────────────────── m = map(lambda x: x**2, [1,2,3]) list(m) # → [1, 4, 9] list(m) # → [] ← iterator exhausted! Store result immediately. # ─── Pitfall 2: Side effects in lambda ───────────────────────── # f = lambda x: print(x); x**2 # SyntaxError — no statements! # Fix: use def for functions with side effects # ─── Pitfall 3: Nested map readability collapse ───────────────── # Hard to read — name your lambdas: add_rows = lambda r1, r2: list(map(lambda a,b: a+b, r1, r2)) good = list(map(add_rows, A, B)) # ← readable # ─── Pitfall 4: Late binding in loops (see §1) ───────────────── fns = [lambda x, i=i: x*i for i in range(3)] # ← correct # ─── Pitfall 5: Lambda when def is cleaner ───────────────────── # WRONG: long, unreadable f = lambda m: sum(map(lambda r: sum(map(lambda x: x**2, r)), m))**0.5 # RIGHT: named function with docstring def frobenius_norm(M): """Frobenius (L2) norm of a matrix.""" return sum(x**2 for row in M for x in row)**0.5
Final Rule Set:
1. Use lambda for short, disposable, single-expression functions.
2. Use map() when applying a function to an iterable — especially lazy or over multiple
iterables.
3. Use list comprehensions for readable single-pass transforms.
4. Name your lambdas when used more than once — it makes them debuggable and faster.
5. Graduate to NumPy the moment performance matters for matrix/tensor work.
Quick Reference Cheat Sheet
All operations from this document in one place — copy-paste ready.
| Operation | map() + lambda Syntax | Result Shape |
|---|---|---|
| Scalar × vector | map(lambda x: x*k, v) |
list (n,) |
| Vector addition | map(lambda a,b: a+b, u, v) |
list (n,) |
| Dot product | sum(map(lambda a,b: a*b, u, v)) |
scalar |
| L1 norm | sum(map(abs, v)) |
scalar |
| L2 norm | sum(map(lambda x: x**2, v))**0.5 |
scalar |
| Lp norm | sum(map(lambda x: abs(x)**p, v))**(1/p) |
scalar |
| L∞ norm | max(map(abs, v)) |
scalar |
| Normalise vector | map(lambda x: x/norm_l2(v), v) |
list (n,) |
| Angle between vectors | math.degrees(math.acos(clamp(cos_sim(a,b)))) |
float ° |
| Outer product | map(lambda x: map(lambda y: x*y, v), u) |
list (m,n) |
| Matrix scalar × | map(lambda r: map(lambda x: x*k, r), M) |
list (m,n) |
| Matrix addition | map(lambda r1,r2: map(add,r1,r2), A,B) |
list (m,n) |
| Transpose 2D | map(list, zip(*M)) |
list (n,m) |
| Hadamard product A⊙B | map(lambda r1,r2: map(lambda a,b: a*b, r1,r2), A,B) |
list (m,n) |
| Matrix–vector multiply | map(lambda row: dot(row,v), M) |
list (m,) |
| Matrix–matrix multiply | map(lambda r: map(lambda c: dot(r,c), Bt), A) |
list (m,p) |
| Trace | sum(map(lambda i: M[i][i], range(len(M)))) |
scalar |
| Diagonal | map(lambda i: M[i][i], range(min(m,n))) |
list (k,) |
| Row sums | map(sum, M) |
list (m,) |
| Column sums | map(sum, zip(*M)) |
list (n,) |
| Apply f element-wise 2D | map(lambda r: map(f, r), M) |
list (m,n) |
| Apply f element-wise 3D | map(lambda l: map(lambda r: map(f,r), l), T) |
list (d,m,n) |
| Flatten 3D → 1D | reduce(lambda a,mat: a+reduce(...), T, []) |
list (d×m×n,) |
| 3D axis-swap (D,R,C)→(C,R,D) | map(c: map(r: map(d: T[d][r][c], …), …), …) |
list (c,r,d) |
| Filter rows by condition | filter(lambda r: cond(r), M) |
filtered rows |
| Sort rows by norm | sorted(M, key=lambda r: norm_l2(r)) |
list (m,n) |
| Multi-key sort | sorted(data, key=lambda x: (-x[1], x[0])) |
sorted list |
| Gradient descent step | map(lambda p,g: p - α*g, θ, grads) |
list (n,) |
| Softmax (vector) | map(lambda e: e/s, map(exp(x-max), v)) |
list (n,) |
| Cosine similarity matrix | map(lambda u: map(lambda v: cosine(u,v), vecs), vecs) |
list (n,n) |
| Compose transforms | reduce(matmul, [T1, T2, T3]) |
list (m,m) |
| Apply pipeline (NumPy) | reduce(lambda M,f: f(M), steps, data) |
ndarray |
| Determinant n×n | sum(map(lambda j: (-1)**j * M[0][j] * det(minor(M,0,j)), range(n))) |
scalar |
| String type-cast | map(int, str_list) / map(float, str_list) |
list (n,) |
| String clean + parse CSV | map(lambda r: map(int, r.strip().split(',')), rows) |
list (m,n) |
Comments
Loading comments...