Advanced Functions
By completing this module you will be able to:
- Use lambda functions and
map/filterfor simple transformations - Implement generators for efficient memory handling
- Create and apply decorators when they actually improve clarity
- Recognize less common techniques such as
reduceand closures
The Next Level
You already know how to create basic functions: define parameters, return values, and organize reusable code. The next step is not learning “tricks”, but understanding which tools deserve regular use and which ones are worth knowing without overusing them.
In this module we will separate those layers:
- Frequent use:
lambda,map,filter, and generators - Situational use: decorators
- Extension material:
reduce, class decorators, and closures
The goal is that you leave this module knowing what to use first, what to leave for later, and which topics deserve a second read once you have more practice.
Lambda Functions
Imagine you’re in a meeting and need to jot something down quickly. You’re not going to pull out a notebook, find a pen, and write a formal note. You grab a Post-it, scribble the essentials, and done.
Lambda functions are exactly that: Python’s Post-it notes. Small, throwaway functions for when defining a complete function with def would be overkill.
1# Normal function (the formal notebook)
2def square(x):
3 return x ** 2
4
5# Equivalent as lambda (the Post-it)
6square = lambda x: x ** 2
7
8print(square(5)) # 25Syntax
1lambda arguments: expressionCommon Use Cases
1# Sort with custom criteria
2people = [
3 {"name": "Ana", "age": 25},
4 {"name": "Luis", "age": 30},
5 {"name": "Maria", "age": 22}
6]
7
8# Sort by age
9sorted_list = sorted(people, key=lambda p: p["age"])
10print(sorted_list)
11
12# Sort by name
13sorted_list = sorted(people, key=lambda p: p["name"])
14
15# Sort list of tuples by second element
16points = [(1, 5), (3, 2), (2, 8)]
17sorted_points = sorted(points, key=lambda p: p[1])
18print(sorted_points) # [(3, 2), (1, 5), (2, 8)]Use lambda for simple one-line functions, especially as arguments to other functions. If the logic is complex, use a regular function with def.
Functional Programming
Imagine an assembly line in a factory. Parts enter one side, pass through different stations that transform or filter them, and finished products come out the other side.
Functional programming applies this same philosophy to code: data flows through functions that transform, filter, or combine it. No side effects, no modifying original data.
graph LR
A["[1,2,3,4,5]"] --> B["map(x²)"]
B --> C["[1,4,9,16,25]"]
C --> D["filter(>5)"]
D --> E["[9,16,25]"]
E --> F["reduce(+)"]
F --> G["50"]Python offers three classic tools for this: map, filter, and reduce.
map(): The Transformer Machine
map() is like a machine that applies the same operation to each piece that passes through. A list goes in, another list of the same size comes out but with each element transformed.
1numbers = [1, 2, 3, 4, 5]
2
3# Calculate squares
4squares = list(map(lambda x: x**2, numbers))
5print(squares) # [1, 4, 9, 16, 25]
6
7# Convert to strings
8texts = list(map(str, numbers))
9print(texts) # ['1', '2', '3', '4', '5']
10
11# With multiple iterables
12a = [1, 2, 3]
13b = [10, 20, 30]
14sums = list(map(lambda x, y: x + y, a, b))
15print(sums) # [11, 22, 33]filter(): Quality Control
filter() is the quality inspector of the assembly line. It examines each piece and decides: does it meet requirements? If yes, it passes. If not, it’s discarded. The resulting list may be smaller than the original.
1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3# Filter evens
4evens = list(filter(lambda x: x % 2 == 0, numbers))
5print(evens) # [2, 4, 6, 8, 10]
6
7# Filter greater than 5
8greater = list(filter(lambda x: x > 5, numbers))
9print(greater) # [6, 7, 8, 9, 10]
10
11# Filter non-empty strings
12words = ["hello", "", "world", "", "python"]
13non_empty = list(filter(None, words)) # None filters "falsy" values
14print(non_empty) # ['hello', 'world', 'python']reduce(): The Compactor Press (less frequent)
reduce() is the machine that compacts everything into one. It takes the first element, combines it with the second, that result with the third, and so on until there’s a single final value.
1from functools import reduce
2
3numbers = [1, 2, 3, 4, 5]
4
5# Cumulative sum
6total = reduce(lambda acc, x: acc + x, numbers)
7print(total) # 15
8
9# Product
10product = reduce(lambda acc, x: acc * x, numbers)
11print(product) # 120
12
13# Find maximum
14maximum = reduce(lambda a, b: a if a > b else b, numbers)
15print(maximum) # 5
16
17# With initial value
18sum_from_100 = reduce(lambda acc, x: acc + x, numbers, 100)
19print(sum_from_100) # 115Comparison: Functional vs Comprehension
1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3# Functional
4even_squares = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers)))
5
6# List comprehension (more readable in Python)
7even_squares = [x**2 for x in numbers if x % 2 == 0]
8
9print(even_squares) # [4, 16, 36, 64, 100]In Python, list comprehensions are usually more readable than map/filter. Use functional programming when it makes sense or when working with existing functions.
Decorators
Imagine you have an already-wrapped gift. Now you want to add a pretty ribbon, shiny paper, and a card. The gift remains the same inside, but now it has extra functionality: it’s prettier, has a message, maybe even makes a sound when shaken.
Decorators do exactly that with functions: they “wrap” them adding extra behavior without modifying their internal code.
graph LR
A["Function call"] --> B["Decorator: BEFORE"]
B --> C["Original function"]
C --> D["Decorator: AFTER"]
D --> E["Result"]Where will you see them in the real world?
@app.route("/")in Flask to define web routes@login_requiredto protect pages@cacheto store results and avoid recalculating@timerto measure how long a function takes
Basic Syntax
1def my_decorator(func):
2 def wrapper(*args, **kwargs):
3 print("Before the function")
4 result = func(*args, **kwargs)
5 print("After the function")
6 return result
7 return wrapper
8
9@my_decorator
10def greet(name):
11 print(f"Hello, {name}!")
12
13greet("Ana")
14# Output:
15# Before the function
16# Hello, Ana!
17# After the functionTiming Decorator
1import time
2
3def measure_time(func):
4 def wrapper(*args, **kwargs):
5 start = time.time()
6 result = func(*args, **kwargs)
7 end = time.time()
8 print(f"{func.__name__} took {end - start:.4f} seconds")
9 return result
10 return wrapper
11
12@measure_time
13def slow_process():
14 time.sleep(1)
15 return "Completed"
16
17result = slow_process()
18# slow_process took 1.0012 secondsDecorator with Parameters
1def repeat(times):
2 def decorator(func):
3 def wrapper(*args, **kwargs):
4 for _ in range(times):
5 result = func(*args, **kwargs)
6 return result
7 return wrapper
8 return decorator
9
10@repeat(3)
11def greet(name):
12 print(f"Hello, {name}")
13
14greet("Ana")
15# Hello, Ana
16# Hello, Ana
17# Hello, AnaPreserving Metadata with functools.wraps
1from functools import wraps
2
3def my_decorator(func):
4 @wraps(func) # Preserves __name__, __doc__, etc.
5 def wrapper(*args, **kwargs):
6 return func(*args, **kwargs)
7 return wrapper
8
9@my_decorator
10def my_function():
11 """This is the documentation."""
12 pass
13
14print(my_function.__name__) # my_function (without @wraps would be 'wrapper')
15print(my_function.__doc__) # This is the documentation.Class Decorators (extension)
1class CallCounter:
2 def __init__(self, func):
3 self.func = func
4 self.calls = 0
5
6 def __call__(self, *args, **kwargs):
7 self.calls += 1
8 print(f"Call #{self.calls}")
9 return self.func(*args, **kwargs)
10
11@CallCounter
12def greet():
13 print("Hello")
14
15greet() # Call #1 / Hello
16greet() # Call #2 / Hello
17print(greet.calls) # 2Generators
Imagine a printing press. If they ask you for a million flyers, you have two options:
- Print everything at once: you fill up an entire warehouse with paper, and if they only needed 100, you’ve wasted resources
- Print on demand: you only produce a flyer when someone asks for it
Generators are option 2. Instead of creating a giant list in memory and returning it whole, they produce values one at a time, only when you ask. They’re “lazy” in the best sense: they don’t work more than necessary.
| Traditional list | Generator |
|---|---|
| Creates everything in memory at once | Produces values one by one |
| Uses memory proportional to size | Uses constant (minimal) memory |
| Can access any element | Can only move forward, not backward |
| Ideal for small data | Ideal for large or infinite data |
Generator Functions with yield
The magic word is yield. Unlike return (which ends the function), yield pauses the function and returns a value. Next time you ask for a value, it continues where it left off.
1def count_to(n):
2 i = 1
3 while i <= n:
4 yield i
5 i += 1
6
7# Use the generator
8for num in count_to(5):
9 print(num) # 1, 2, 3, 4, 5
10
11# The generator is lazy
12gen = count_to(1000000)
13print(next(gen)) # 1
14print(next(gen)) # 2Generator Expressions
1# List (uses memory)
2squares_list = [x**2 for x in range(1000000)]
3
4# Generator (doesn't use memory until accessed)
5squares_gen = (x**2 for x in range(1000000))
6
7# Iterate over the generator
8for i, square in enumerate(squares_gen):
9 if i >= 5:
10 break
11 print(square) # 0, 1, 4, 9, 16Generators for Large Files
1def read_in_chunks(filename, size=1024):
2 """Read a file in chunks to save memory."""
3 with open(filename, 'r') as f:
4 while True:
5 chunk = f.read(size)
6 if not chunk:
7 break
8 yield chunk
9
10# Process large file without loading it all into memory
11for chunk in read_in_chunks('large_file.txt'):
12 process(chunk)yield from
1def nested_generator():
2 yield from [1, 2, 3]
3 yield from [4, 5, 6]
4
5for num in nested_generator():
6 print(num) # 1, 2, 3, 4, 5, 6Higher-Order Functions and Closures (extension)
Until now, we’ve treated functions as tools: you define them, call them, they return something. But in Python, functions are first-class citizens. This means you can:
- Store them in variables
- Pass them as arguments to other functions
- Return them as results from other functions
This last point is what we call higher-order functions: functions that create and return other functions.
1def create_multiplier(factor):
2 def multiply(x):
3 return x * factor
4 return multiply
5
6double = create_multiplier(2)
7triple = create_multiplier(3)
8
9print(double(5)) # 10
10print(triple(5)) # 15Closures: Functions with Memory
Here’s where it gets interesting. In the example above, when we call double(5), the multiply function needs the value of factor. But create_multiplier already finished executing… where does factor come from?
The answer is closures. An inner function “remembers” the variables from the scope where it was created, even after that scope has ended.
Think of an employee who receives instructions on their first day (“multiply everything by 2”) and remembers them forever, even though the boss who gave them is no longer there.
1def counter():
2 count = 0
3 def increment():
4 nonlocal count
5 count += 1
6 return count
7 return increment
8
9my_counter = counter()
10print(my_counter()) # 1
11print(my_counter()) # 2
12print(my_counter()) # 3When to Use Each Technique?
| Technique | Use it when… | Typical example |
|---|---|---|
| Lambda | You need a simple throwaway function | sorted(list, key=lambda x: x[“age”]) |
| map/filter | You transform or filter data lists | Convert prices, filter active users |
| reduce | You reduce a list to a single value | Sum totals, find maximum |
| Decorators | You want to add behavior without modifying the function | Logging, cache, authentication, timing |
| Generators | You work with large or infinite data | Read huge files, data streams |
| Closures | You need functions that “remember” values | Create custom functions, counters |
Don’t use these techniques just for the sake of it. If a simple for loop solves your problem clearly, use it. These tools shine in specific situations, they’re not a universal replacement.
Practical Exercises
Given a list of prices stored as text, convert them to numbers, discard empty values, and return a list with tax applied.
Create a generator that receives a list and yields fixed-size batches so you can process data lazily instead of handling everything at once.
Implement a decorator that caches a function’s results to avoid recalculating.