Docs
README
10 - Advanced Functions
📌 What You'll Learn
- •Closures
- •Decorators in depth
- •Generators and
yield - •Iterators and iterable protocol
- •Higher-order functions
- •Functional programming concepts
🔒 Closures
A closure is a function that remembers the values from its enclosing scope even after that scope has finished executing.
def outer_function(x):
# x is "enclosed" in the inner function
def inner_function(y):
return x + y
return inner_function
# Create closures
add_5 = outer_function(5)
add_10 = outer_function(10)
print(add_5(3)) # 8 (5 + 3)
print(add_10(3)) # 13 (10 + 3)
# x is remembered even after outer_function finished!
Practical Closure Example
def make_counter():
count = 0
def counter():
nonlocal count # Access outer variable
count += 1
return count
return counter
# Create independent counters
counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (independent!)
🎀 Decorators In Depth
Basic Decorator Pattern
def my_decorator(func):
def wrapper(*args, **kwargs):
# Before function call
print("Before")
# Call original function
result = func(*args, **kwargs)
# After function call
print("After")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
return name
say_hello("Alice")
Preserving Function Metadata
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""This is my function."""
pass
print(my_function.__name__) # my_function (not 'wrapper')
print(my_function.__doc__) # This is my function.
Decorator with Arguments
def repeat(times):
"""Decorator factory that takes arguments."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice") # Prints 3 times
Class-Based Decorator
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call {self.count} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # Call 1
say_hello() # Call 2
Stacking Decorators
def bold(func):
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("World")) # <b><i>Hello, World</i></b>
Common Decorators
import time
from functools import wraps
# Timer decorator
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end-start:.4f}s")
return result
return wrapper
# Memoization decorator
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
# Retry decorator
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt+1} failed: {e}")
return wrapper
return decorator
🔄 Generators
Generators are functions that can pause and resume execution, yielding values one at a time.
Basic Generator
def count_up_to(n):
i = 1
while i <= n:
yield i # Pause and return value
i += 1
# Use generator
for num in count_up_to(5):
print(num) # 1, 2, 3, 4, 5
# Or manually
gen = count_up_to(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
# print(next(gen)) # StopIteration
Generator Benefits
# Memory efficient - generates values on demand
# List: stores everything in memory
def get_squares_list(n):
return [x**2 for x in range(n)]
# Generator: generates one at a time
def get_squares_gen(n):
for x in range(n):
yield x**2
# For large n, generator uses constant memory
Generator Expression
# Like list comprehension but with ()
squares_gen = (x**2 for x in range(10))
# vs list comprehension
squares_list = [x**2 for x in range(10)]
for sq in squares_gen:
print(sq)
Generator Pipeline
def numbers():
for i in range(10):
yield i
def square(nums):
for n in nums:
yield n ** 2
def filter_evens(nums):
for n in nums:
if n % 2 == 0:
yield n
# Chain generators
pipeline = filter_evens(square(numbers()))
print(list(pipeline)) # [0, 4, 16, 36, 64]
yield from
def nested_generator():
yield from range(3) # 0, 1, 2
yield from 'abc' # 'a', 'b', 'c'
yield from [10, 20, 30] # 10, 20, 30
for item in nested_generator():
print(item)
🔁 Iterators
An iterator is an object that implements __iter__ and __next__ methods.
Creating an Iterator Class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# Use the iterator
for num in Countdown(5):
print(num) # 5, 4, 3, 2, 1
iter() and next()
# Make any iterable into an iterator
my_list = [1, 2, 3]
my_iter = iter(my_list)
print(next(my_iter)) # 1
print(next(my_iter)) # 2
print(next(my_iter)) # 3
🎯 Higher-Order Functions
Functions that take functions as arguments or return functions.
Built-in Higher-Order Functions
# map() - apply function to each element
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
# filter() - keep elements matching condition
evens = list(filter(lambda x: x % 2 == 0, numbers))
# reduce() - accumulate values
from functools import reduce
total = reduce(lambda x, y: x + y, numbers)
product = reduce(lambda x, y: x * y, numbers)
sorted() with key
words = ['banana', 'apple', 'cherry', 'date']
# Sort by length
by_length = sorted(words, key=len)
# Sort by last character
by_last = sorted(words, key=lambda x: x[-1])
# Sort objects
students = [
{'name': 'Alice', 'grade': 85},
{'name': 'Bob', 'grade': 92},
]
by_grade = sorted(students, key=lambda x: x['grade'])
Creating Higher-Order Functions
def apply_operation(operation):
"""Returns a function that applies operation to a list."""
def apply_to_list(lst):
return [operation(x) for x in lst]
return apply_to_list
double = apply_operation(lambda x: x * 2)
square = apply_operation(lambda x: x ** 2)
print(double([1, 2, 3])) # [2, 4, 6]
print(square([1, 2, 3])) # [1, 4, 9]
🧩 Functional Programming Concepts
Pure Functions
# Pure: same input always gives same output, no side effects
def pure_add(a, b):
return a + b
# Impure: has side effects
total = 0
def impure_add(value):
global total
total += value
return total
Partial Functions
from functools import partial
def power(base, exponent):
return base ** exponent
# Create specialized functions
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
Function Composition
def compose(*functions):
def inner(arg):
result = arg
for f in reversed(functions):
result = f(result)
return result
return inner
add_one = lambda x: x + 1
double = lambda x: x * 2
square = lambda x: x ** 2
composed = compose(square, double, add_one)
print(composed(3)) # ((3+1)*2)^2 = 64
📋 Summary
| Concept | Description |
|---|---|
| Closure | Function remembering enclosing scope |
| Decorator | Function modifying another function |
| Generator | Function that yields values lazily |
| Iterator | Object with __iter__ and __next__ |
| map() | Apply function to all elements |
| filter() | Keep elements matching condition |
| reduce() | Accumulate values |
| partial() | Create specialized function |
🎯 Next Steps
After mastering advanced functions, proceed to 11_advanced_data to learn about shallow vs deep copy, collections module, and memory management!