Phase: 4. Functions | Estimated time: 2 hours | Milestone Project: No
- Module 031 (Functions: Basics)
- Module 033 (Scope and Namespaces)
- Module 036 (Higher-Order Functions)
By the end of this module, you will be able to:
- Understand functions as first-class objects
- Create nested functions (closures)
- Use the
@decoratorsyntax - Write simple decorators (timing, logging)
- Use
functools.wrapsto preserve metadata - Apply multiple decorators
Decorators are one of Python's most powerful features. They let you modify or enhance functions without changing their code — perfect for logging, access control, caching, and timing.
def greet(name):
return f"Hello, {name}!"
f = greet # assign to variable
print(f("Alice"))A function defined inside another can access the outer function's variables:
def outer(msg):
def inner(name):
return f"{msg}, {name}!"
return inner
hello = outer("Hello")
print(hello("Bob"))A decorator is a function that takes another function and extends it:
def decorator(func):
def wrapper(*args, **kwargs):
# do something before
result = func(*args, **kwargs)
# do something after
return result
return wrapper
@decorator
def say_hello():
print("Hello!")import time
def timer(func):
"""Measure and print the execution time of a function."""
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.5)Without @wraps, the decorated function loses its original name and docstring:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper docstring."""
return func(*args, **kwargs)
return wrapperOriginal function: greet("Alice") → "Hello, Alice!"
After @logger: logger(greet) → wrapper("Alice")
1. log "calling greet"
2. greet("Alice") → "Hello, Alice!"
3. log "finished greet"
4. return result
@decorator1
@decorator2
def func():
pass
# Equivalent to: func = decorator1(decorator2(func))- Forgetting
@wraps: Breaks introspection (help(), name, doc). - Forgetting
*args, **kwargs: Decorator won't work with arbitrary arguments. - Not returning the wrapper: The decorator must return a function.
- Mutable closure variables: Can cause surprising behavior.
- Write a decorator
@loggerthat prints "Calling func_name" before a function runs. - Apply it to a simple function and test.
- Add
@wrapsand verify__name__is preserved. - Stack two decorators:
@timerand@logger.
- Decorators wrap functions to add behavior without modifying the original.
@decoratoris syntactic sugar forfunc = decorator(func).- Always use
@wrapsto preserve function metadata. - Decorators stack bottom-up (closest to function applied first).
- Use
*args, **kwargsfor maximum flexibility.
Continue to Module 038: Generators and yield.