Timothy had mastered functions that remembered their environment through closures, but Margaret had one more revelation waiting. She led him to a workshop labeled "The Function Modification Station"—a place where existing procedures could be enhanced with new capabilities without rewriting them.
The Enhancement Request
The head librarian approached Timothy with a problem: "Every cataloging function needs to log when it runs and how long it takes. Can you add this to all our procedures without changing their core logic?"
Timothy's first instinct was to modify each function directly:
import time
def catalog_book(title, author):
start_time = time.time()
print(f"Running catalog_book at {start_time}")
# Actual cataloging work
result = f"Cataloged: {title} by {author}"
end_time = time.time()
print(f"Completed in {end_time - start_time:.2f} seconds")
return result
But with dozens of functions needing this same enhancement, copying the logging code everywhere felt wrong. Margaret showed him a better way.
The Wrapper Concept
"What if," Margaret suggested, "we could wrap one function inside another? The outer function adds the logging, while the inner function does the original work."
def add_logging(original_function):
def wrapper(*args, **kwargs):
start_time = time.time()
print(f"Running {original_function.__name__}")
result = original_function(*args, **kwargs)
elapsed = time.time() - start_time
print(f"Completed in {elapsed:.2f} seconds")
return result
return wrapper
Timothy studied the pattern. The add_logging
function accepted another function as input and returned a new function that wrapped the original with logging code.
def catalog_book(title, author):
return f"Cataloged: {title} by {author}"
# Wrap the function
catalog_book = add_logging(catalog_book)
# Now it logs automatically
catalog_book("1984", "Orwell")
# Running catalog_book
# Completed in 0.00 seconds
The enhanced version executed the logging code before and after the original function, but the cataloging logic remained unchanged.
The @ Syntax Sugar
Margaret revealed Python's shorthand for this pattern: the decorator syntax.
@add_logging
def catalog_book(title, author):
return f"Cataloged: {title} by {author}"
@add_logging
def search_catalog(query):
return f"Searching for: {query}"
The @add_logging
line was equivalent to catalog_book = add_logging(catalog_book)
, but cleaner and more explicit. The decorator sat right above the function definition, announcing "this function is enhanced with logging."
The Preservation Challenge
Timothy discovered a problem when inspecting decorated functions:
print(catalog_book.__name__) # "wrapper" - not helpful!
The wrapper function had replaced the original's metadata. Margaret showed him the solution:
from functools import wraps
def add_logging(original_function):
@wraps(original_function)
def wrapper(*args, **kwargs):
start_time = time.time()
print(f"Running {original_function.__name__}")
result = original_function(*args, **kwargs)
elapsed = time.time() - start_time
print(f"Completed in {elapsed:.2f} seconds")
return result
return wrapper
The @wraps
decorator preserved the original function's name, docstring, and other metadata, making debugging and introspection work correctly.
The Multiple Enhancement Pattern
Timothy learned decorators could be stacked:
def add_logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def require_permission(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not user_has_permission():
raise PermissionError("Access denied")
return func(*args, **kwargs)
return wrapper
@add_logging
@require_permission
def delete_book(book_id):
return f"Deleted book {book_id}"
The decorators applied from bottom to top: require_permission
wrapped delete_book
first, then add_logging
wrapped the result. The function gained both permission checking and logging.
The Practical Applications
Timothy compiled common decorator patterns:
Timing functions:
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__}: {time.time() - start:.4f}s")
return result
return wrapper
Caching results:
def cache_results(func):
cached = {}
@wraps(func)
def wrapper(*args):
if args not in cached:
cached[args] = func(*args)
return cached[args]
return wrapper
@cache_results
def expensive_calculation(n):
return sum(range(n))
Validating inputs:
def validate_positive(func):
@wraps(func)
def wrapper(number):
if number <= 0:
raise ValueError("Number must be positive")
return func(number)
return wrapper
@validate_positive
def process_count(count):
return f"Processing {count} items"
The Class Method Decorators
Margaret showed Timothy that Python included built-in decorators for class methods:
class Library:
total_books = 0
@classmethod
def get_total_books(cls):
return cls.total_books
@staticmethod
def format_title(title):
return title.upper()
@property
def book_count(self):
return len(self.books)
These decorators transformed how methods behaved—@classmethod
received the class itself, @staticmethod
acted like a regular function, and @property
made methods accessible like attributes.
The Mental Model
Timothy developed a clear understanding: decorators were functions that transformed other functions. They followed a consistent pattern:
- Accept a function as input
- Create a wrapper function that adds behavior
- Return the wrapper to replace the original
- Use
@wraps
to preserve metadata
The @
syntax was just syntactic sugar for the function wrapping pattern, making the enhancement visible and declarative.
Timothy's Decorator Wisdom
Through mastering the Function Modification Station, Timothy learned essential principles:
Decorators enhance without changing core logic: Add cross-cutting concerns (logging, timing, validation) without modifying function internals.
The @ syntax is transformation: @decorator
means "replace this function with the decorated version."
Preserve metadata with @wraps: Always use functools.wraps
to maintain function identity.
Decorators stack bottom-up: Multiple decorators apply from bottom to top.
Common patterns emerge: Timing, caching, validation, and access control are natural decorator use cases.
Timothy's exploration revealed that decorators embodied Python's philosophy: make common patterns easy to express. Rather than copying enhancement code across functions, decorators let you declare enhancements once and apply them declaratively. The Function Modification Station transformed ordinary procedures into enhanced versions while keeping the core logic clean and focused.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)