1"""
2Functional Programming in Python
3
4Demonstrates:
5- functools module (partial, reduce, lru_cache)
6- operator module
7- map, filter, zip
8- Lambda functions
9- Function composition
10- Immutability patterns
11- Higher-order functions
12"""
13
14import functools
15import operator
16from typing import Callable, Iterable, Any, List
17
18
19def section(title: str) -> None:
20 """Print a section header."""
21 print("\n" + "=" * 60)
22 print(f" {title}")
23 print("=" * 60)
24
25
26# =============================================================================
27# Lambda Functions
28# =============================================================================
29
30section("Lambda Functions")
31
32# Basic lambda
33add = lambda x, y: x + y
34print(f"add(3, 5) = {add(3, 5)}")
35
36# Lambda with map
37numbers = [1, 2, 3, 4, 5]
38squared = list(map(lambda x: x**2, numbers))
39print(f"map(lambda x: x**2, {numbers}) = {squared}")
40
41# Lambda with filter
42evens = list(filter(lambda x: x % 2 == 0, numbers))
43print(f"filter(lambda x: x % 2 == 0, {numbers}) = {evens}")
44
45# Lambda with sorted
46words = ["apple", "pie", "zoo", "car"]
47sorted_by_length = sorted(words, key=lambda x: len(x))
48print(f"sorted by length: {sorted_by_length}")
49
50
51# =============================================================================
52# map, filter, zip
53# =============================================================================
54
55section("map, filter, zip")
56
57# map - apply function to all items
58numbers = [1, 2, 3, 4, 5]
59doubled = list(map(lambda x: x * 2, numbers))
60print(f"Doubled: {doubled}")
61
62# Multiple iterables
63a = [1, 2, 3]
64b = [10, 20, 30]
65summed = list(map(lambda x, y: x + y, a, b))
66print(f"map(add, {a}, {b}) = {summed}")
67
68# filter - keep items matching predicate
69numbers = range(10)
70evens = list(filter(lambda x: x % 2 == 0, numbers))
71print(f"Even numbers: {evens}")
72
73# zip - combine iterables
74names = ["Alice", "Bob", "Charlie"]
75ages = [25, 30, 35]
76combined = list(zip(names, ages))
77print(f"Zipped: {combined}")
78
79# Unzip with zip(*...)
80unzipped_names, unzipped_ages = zip(*combined)
81print(f"Unzipped names: {list(unzipped_names)}")
82print(f"Unzipped ages: {list(unzipped_ages)}")
83
84
85# =============================================================================
86# functools.partial
87# =============================================================================
88
89section("functools.partial")
90
91
92def power(base: float, exponent: float) -> float:
93 """Raise base to exponent."""
94 return base ** exponent
95
96
97# Create specialized functions
98square = functools.partial(power, exponent=2)
99cube = functools.partial(power, exponent=3)
100
101print(f"square(5) = {square(5)}")
102print(f"cube(3) = {cube(3)}")
103
104
105def multiply(x: float, y: float, z: float) -> float:
106 """Multiply three numbers."""
107 return x * y * z
108
109
110# Partial application
111double = functools.partial(multiply, 2)
112print(f"double(3, 4) = {double(3, 4)}")
113
114triple_and_double = functools.partial(multiply, 2, 3)
115print(f"triple_and_double(5) = {triple_and_double(5)}")
116
117
118# =============================================================================
119# functools.reduce
120# =============================================================================
121
122section("functools.reduce")
123
124numbers = [1, 2, 3, 4, 5]
125
126# Sum using reduce
127total = functools.reduce(lambda x, y: x + y, numbers)
128print(f"Sum of {numbers} = {total}")
129
130# Product using reduce
131product = functools.reduce(lambda x, y: x * y, numbers)
132print(f"Product of {numbers} = {product}")
133
134# With initial value
135result = functools.reduce(lambda x, y: x + y, numbers, 100)
136print(f"Sum with initial value 100: {result}")
137
138# Find maximum
139max_value = functools.reduce(lambda x, y: x if x > y else y, numbers)
140print(f"Maximum: {max_value}")
141
142
143# =============================================================================
144# operator Module
145# =============================================================================
146
147section("operator Module")
148
149# Arithmetic operators as functions
150print(f"operator.add(3, 5) = {operator.add(3, 5)}")
151print(f"operator.mul(4, 7) = {operator.mul(4, 7)}")
152print(f"operator.pow(2, 10) = {operator.pow(2, 10)}")
153
154# Comparison operators
155print(f"operator.gt(5, 3) = {operator.gt(5, 3)}")
156print(f"operator.eq(5, 5) = {operator.eq(5, 5)}")
157
158# Item getters
159data = [10, 20, 30, 40]
160get_second = operator.itemgetter(1)
161print(f"itemgetter(1) on {data} = {get_second(data)}")
162
163# Multiple items
164get_items = operator.itemgetter(0, 2)
165print(f"itemgetter(0, 2) on {data} = {get_items(data)}")
166
167# Attribute getter
168from collections import namedtuple
169Person = namedtuple('Person', ['name', 'age'])
170people = [Person('Alice', 30), Person('Bob', 25), Person('Charlie', 35)]
171
172get_name = operator.attrgetter('name')
173names = list(map(get_name, people))
174print(f"Names: {names}")
175
176# Sort by attribute
177sorted_people = sorted(people, key=operator.attrgetter('age'))
178print(f"Sorted by age: {[f'{p.name}({p.age})' for p in sorted_people]}")
179
180
181# =============================================================================
182# functools.lru_cache
183# =============================================================================
184
185section("functools.lru_cache")
186
187
188@functools.lru_cache(maxsize=128)
189def fibonacci(n: int) -> int:
190 """Fibonacci with memoization."""
191 if n <= 1:
192 return n
193 return fibonacci(n - 1) + fibonacci(n - 2)
194
195
196print("Computing Fibonacci numbers with caching:")
197for i in [10, 20, 10, 15]: # 10 will use cache on second call
198 result = fibonacci(i)
199 print(f" fib({i}) = {result}")
200
201print(f"\nCache info: {fibonacci.cache_info()}")
202fibonacci.cache_clear()
203print("Cache cleared")
204
205
206# =============================================================================
207# Higher-Order Functions
208# =============================================================================
209
210section("Higher-Order Functions")
211
212
213def apply_twice(func: Callable, x: Any) -> Any:
214 """Apply function twice."""
215 return func(func(x))
216
217
218def add_five(x: int) -> int:
219 return x + 5
220
221
222result = apply_twice(add_five, 10)
223print(f"apply_twice(add_five, 10) = {result}")
224
225
226def make_multiplier(factor: float) -> Callable:
227 """Return a function that multiplies by factor."""
228 def multiplier(x: float) -> float:
229 return x * factor
230 return multiplier
231
232
233times_three = make_multiplier(3)
234times_ten = make_multiplier(10)
235
236print(f"times_three(5) = {times_three(5)}")
237print(f"times_ten(5) = {times_ten(5)}")
238
239
240# =============================================================================
241# Function Composition
242# =============================================================================
243
244section("Function Composition")
245
246
247def compose(*functions):
248 """Compose functions right to left."""
249 def inner(arg):
250 result = arg
251 for func in reversed(functions):
252 result = func(result)
253 return result
254 return inner
255
256
257def add_one(x: int) -> int:
258 return x + 1
259
260
261def multiply_by_two(x: int) -> int:
262 return x * 2
263
264
265def square(x: int) -> int:
266 return x ** 2
267
268
269# Compose: square(multiply_by_two(add_one(x)))
270composed = compose(square, multiply_by_two, add_one)
271result = composed(5) # ((5 + 1) * 2) ** 2 = (6 * 2) ** 2 = 144
272print(f"composed(5) = {result}")
273
274
275def pipe(*functions):
276 """Compose functions left to right."""
277 def inner(arg):
278 result = arg
279 for func in functions:
280 result = func(result)
281 return result
282 return inner
283
284
285# Pipe: add_one -> multiply_by_two -> square
286piped = pipe(add_one, multiply_by_two, square)
287result = piped(5) # Same as compose example
288print(f"piped(5) = {result}")
289
290
291# =============================================================================
292# List Comprehensions vs Functional Style
293# =============================================================================
294
295section("List Comprehensions vs Functional Style")
296
297numbers = range(1, 11)
298
299# Comprehension style
300result_comp = [x**2 for x in numbers if x % 2 == 0]
301print(f"Comprehension: {result_comp}")
302
303# Functional style
304result_func = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers)))
305print(f"Functional: {result_func}")
306
307# Nested comprehension
308matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
309flattened_comp = [item for row in matrix for item in row]
310print(f"Flattened (comp): {flattened_comp}")
311
312# Functional style
313flattened_func = functools.reduce(operator.add, matrix)
314print(f"Flattened (func): {flattened_func}")
315
316
317# =============================================================================
318# Immutability Patterns
319# =============================================================================
320
321section("Immutability Patterns")
322
323# Tuples are immutable
324point = (10, 20)
325print(f"Original point: {point}")
326
327# Create new tuple instead of modifying
328moved_point = (point[0] + 5, point[1] + 3)
329print(f"Moved point: {moved_point}")
330
331# Frozen sets
332frozen = frozenset([1, 2, 3, 4])
333print(f"Frozen set: {frozen}")
334
335# Named tuples for immutable records
336from collections import namedtuple
337Point = namedtuple('Point', ['x', 'y'])
338p1 = Point(10, 20)
339print(f"NamedTuple: {p1}")
340
341# "Update" by creating new instance
342p2 = p1._replace(x=15)
343print(f"Updated: {p2} (original: {p1})")
344
345
346# =============================================================================
347# Currying Pattern
348# =============================================================================
349
350section("Currying Pattern")
351
352
353def curry(func: Callable) -> Callable:
354 """Convert function to curried form."""
355 @functools.wraps(func)
356 def curried(*args, **kwargs):
357 if len(args) + len(kwargs) >= func.__code__.co_argcount:
358 return func(*args, **kwargs)
359 return functools.partial(curried, *args, **kwargs)
360 return curried
361
362
363@curry
364def add_three(a: int, b: int, c: int) -> int:
365 """Add three numbers."""
366 return a + b + c
367
368
369# Can call with all args at once
370print(f"add_three(1, 2, 3) = {add_three(1, 2, 3)}")
371
372# Or curry it
373add_1 = add_three(1)
374add_1_2 = add_1(2)
375result = add_1_2(3)
376print(f"Curried: {result}")
377
378
379# =============================================================================
380# Pure Functions
381# =============================================================================
382
383section("Pure Functions")
384
385
386# Pure function - no side effects, deterministic
387def pure_sum(numbers: List[int]) -> int:
388 """Pure function - always returns same output for same input."""
389 return sum(numbers)
390
391
392# Impure function - modifies external state
393counter = 0
394
395
396def impure_increment():
397 """Impure function - has side effects."""
398 global counter
399 counter += 1
400 return counter
401
402
403data = [1, 2, 3, 4, 5]
404print(f"pure_sum({data}) = {pure_sum(data)}")
405print(f"pure_sum({data}) = {pure_sum(data)}") # Same result
406
407print(f"impure_increment() = {impure_increment()}")
408print(f"impure_increment() = {impure_increment()}") # Different result
409
410
411# =============================================================================
412# Summary
413# =============================================================================
414
415section("Summary")
416
417print("""
418Functional programming patterns covered:
4191. Lambda functions - anonymous functions
4202. map, filter, zip - transform iterables
4213. functools.partial - partial application
4224. functools.reduce - accumulate values
4235. operator module - operators as functions
4246. functools.lru_cache - memoization
4257. Higher-order functions - functions as arguments/return values
4268. Function composition - combine functions
4279. Immutability - prefer immutable data structures
42810. Pure functions - no side effects
429
430Benefits of functional programming:
431- More predictable code (pure functions)
432- Easier to test and reason about
433- Better composability
434- Natural parallelization (no shared state)
435
436Python supports functional programming but isn't purely functional.
437Use functional patterns where they improve code clarity.
438""")