Python · Functional Programming · ML

The Complete Guide to Python's map() & lambda

Python map and 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.

01

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

lambda arg₁, arg₂, ..., argₙ : expression

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])]
02

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.

map(f, [x₀, x₁, ..., xₙ]) [f(x₀), f(x₁), ..., f(xₙ)]

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)
03

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.

04

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·Amap(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)
05

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.

[
a₀₀
a₀₁
a₀₂
a₁₀
a₁₁
a₁₂
a₂₀
a₂₁
a₂₂
]
3×3 matrix · diagonal highlighted

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

(Av)i = Σₖ Aik · vk    ←→    dot(row_i, v)

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

(u ⊗ v)ij = ui · vj         m×n matrix

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

C[i][j] = Σₖ A[i][k] · B[k][j]    ←→    dot(row_i, col_j)

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
06

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]]
07

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

softmax(z)i = eᶻⁱ / Σⱼ eᶻʲ

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]
08

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
09

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.

10

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.

11

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

Comments

Loading comments...