Decorator Implementation
Problem Statement
Implement a simple function decorator.
Background
Decorators wrap functions to add behavior:
@decorator
def func():
pass
# Equivalent to: func = decorator(func)
Your Task
Write a decorator call_counter that counts how many times a function is called.
The decorated function should have an attribute call_count that tracks the count.
Output Format
The decorated function should work normally but have a call_count attribute.
Example:
Call greet twice
call_count = 2
Each call increments the counter stored on the wrapper function
Constraints:
- Preserve the original function's behavior
- Add call_count attribute to the wrapper
1. Background Knowledge
Function Decorators are a Python feature for modifying function behavior without changing their core logic. Syntactically, @decorator above a function is equivalent to func = decorator(func), where the decorator is itself a function that takes the original function as input and returns a wrapper function. The wrapper executes additional code (pre/post-processing) around the original call while preserving the original's signature, return value, and attributes.
Key concepts include:
- Closures: Inner functions accessing outer scope variables (e.g., a counter variable in the wrapper).
- Function attributes: Python functions are objects that can store custom attributes like call_count.
- Decorators must use *args, **kwargs to handle any argument count/type, ensuring generality.
This pattern enables cross-cutting concerns like logging, timing, or caching without altering the decorated function's code.
2. Algorithm/Approach
The wrapper pattern via closures:
- Define call_counter() that accepts the original function func.
- Return a wrapper function that:
- Increments a mutable counter (e.g., list or class instance) stored in the closure.
- Calls func(*args, **kwargs) to execute original logic.
- Attach call_count attribute to the returned wrapper (not the original func).
- Preserve original metadata using functools.wraps(func) (optional but recommended for introspection).
This creates a stateful wrapper where call_count persists across calls due to closure scope.
3. Step-by-Step Strategy
- Define the decorator factory: Create call_counter(func) that returns a wrapper.
- Initialize state: Use a mutable object (e.g., ) in the outer scope to track calls.
- Implement wrapper logic:
- Increment counter.
- Forward *args, **kwargs to func.
- Return func's result.
- Add attribute: Set wrapper.call_count = counter (or use a property for reactivity).
- Handle preservation: Import functools and apply @wraps(func) to the wrapper to copy metadata like name, doc.
- Test: Apply @call_counter, call multiple times, access greet.call_count.
Example skeleton (not full solution):
from functools import wraps
def call_counter(func):
count = # Mutable state
@wraps(func)
def wrapper(*args, **kwargs):
count += 1
wrapper.call_count = count # Dynamic attribute
return func(*args, **kwargs)
return wrapper
4. Common Pitfalls
- Immutable state: Using count = 0 (int) fails as reassignment doesn't update closure; use count = .
- Attribute on wrong object: Set wrapper.call_count, not func.call_count—users access the wrapper post-decoration.
- Signature mismatch: Forgetting *args, **kwargs breaks functions with varying args.
- Lost metadata: Without @wraps, wrapper.name becomes 'wrapper', breaking tools like debuggers.
- Non-reactive count: Static assignment shows stale value; update inside wrapper or use @property.
- Infinite recursion: Accidentally calling wrapper instead of func inside wrapper.
5. Time & Space Complexity
- Time: Each decorated call adds O(1) overhead (increment + attribute set), independent of original function complexity.
- Space: O(1) extra per decorator instance (closure + mutable counter); scales with number of unique decorated functions, not calls.
This is efficient for production use, as decorators add negligible runtime cost.