The @tracer
decorator is a debugging and monitoring tool that allows you to trace the execution of functions in Python. Its main purposes are:
- Function Execution Tracking: It helps monitor when functions are called, what arguments they receive, and what values they return.
- Debugging Aid: It can print out detailed information about function calls, making it easier to debug complex programs.
Here’s an example implementation of a basic @tracer
decorator:
# tracer_example.py
def tracer(func):
def wrapper(*args, **kwargs):
# Print function entry
print(f"Entering function: {func.__name__}")
print(f"Arguments: args={args}, kwargs={kwargs}")
# Execute the function
result = func(*args, **kwargs)
# Print function exit
print(f"Exiting function: {func.__name__}")
print(f"Return value: {result}")
return result
return wrapper
# Example usage
@tracer
def greet(name):
return f"Hello, {name}!"
# When you call greet("Alice"), you'll see the tracing output
result = greet("Alice")
When you use this decorator, it will output something like:
Entering function: greet
Arguments: args=('Alice',), kwargs={}
Exiting function: greet
Return value: Hello, Alice!
Key benefits of using the @tracer
decorator:
- Non-invasive Debugging: You can add and remove the decorator without changing the function’s code.
- Consistent Logging: Provides a standardized way to log function calls across your codebase.
- Performance Profiling: Can be extended to measure function execution time.
- Call Stack Analysis: Helps understand the flow of execution in your program.
You can also create more sophisticated tracers that:
- Track recursion depth
- Measure execution time
- Log to files instead of printing
- Add indentation to show call hierarchy
- Filter certain types of functions or arguments
The @tracer
decorator is particularly useful during development and debugging phases, but it’s typically removed or disabled in production code to avoid performance overhead and unnecessary logging.
Advanced Tracer
You can create a parameterized version of the @tracer decorator that optionally includes call stack information. Here’s how you can implement it:
# advanced_tracer_example.py
import traceback
import functools
from typing import Optional
def tracer(show_stack: Optional[bool] = False):
"""
A parameterized decorator that traces function calls and optionally shows the call stack.
Args:
show_stack (bool, optional): If True, will include the call stack in the trace output. Defaults to False.
"""
def decorator(func):
@functools.wraps(func) # Preserves the original function's metadata
def wrapper(*args, **kwargs):
# Print function entry
print(f"\nEntering function: {func.__name__}")
print(f"Arguments: args={args}, kwargs={kwargs}")
# Show call stack if requested
if show_stack:
print("\nCall Stack:")
# Get the call stack excluding this wrapper function
stack = traceback.extract_stack()[:-1] # Remove last entry (this wrapper)
for filename, lineno, name, line in stack:
print(f" File {filename}, line {lineno}, in {name}")
if line:
print(f" {line.strip()}")
# Execute the function
try:
result = func(*args, **kwargs)
# Print function exit
print(f"\nExiting function: {func.__name__}")
print(f"Return value: {result}")
return result
except Exception as e:
print(f"\nException in function {func.__name__}:")
if show_stack:
print("\nException Stack Trace:")
traceback.print_exc()
raise # Re-raise the exception
return wrapper
return decorator
# Example usage with and without stack trace
@tracer() # Without stack trace
def simple_function(x: int, y: int):
return x + y
@tracer(show_stack=True) # With stack trace
def recursive_function(n: int):
if n <= 1:
return 1
return n * recursive_function(n - 1)
@tracer(show_stack=True) # Exception, with stack trace
def keyerror_function(d: dict, key: str):
print(d[key])
# Usage example
if __name__ == "__main__":
# Test simple function
print("\nTesting simple function:")
result = simple_function(5, 3)
print()
print("=" * 30)
# Test recursive function with stack trace
print("\nTesting recursive function:")
result = recursive_function(3)
print()
print("=" * 30)
# Test KeyError Exception
d = {"found": 1}
keyerror_function(d, "notfound")
Key features of this implementation:
- Optional Parameter: The decorator can be used with or without parameters:
@tracer() # Without stack trace
@tracer(show_stack=True) # With stack trace
- Stack Trace Information: When
show_stack=True
, it shows:
- The call stack leading to the function
- File names and line numbers
- The actual lines of code being executed
Exception Handling: Captures and displays exceptions with stack traces if enabled
Proper Function Wrapping: Uses
@functools.wraps
to preserve the original function’s metadata
Example output for the recursive function with stack trace enabled:
Traceback (most recent call last):
File "/Users/john/h/06/advanced_tracer_example.py", line 32, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/john/h/06/advanced_tracer_example.py", line 62, in keyerror_function
print(d[key])
~^^^^^
KeyError: 'notfound'
Testing simple function:
Entering function: simple_function
Arguments: args=(5, 3), kwargs={}
Exiting function: simple_function
Return value: 8
==============================
Testing recursive function:
Entering function: recursive_function
Arguments: args=(3,), kwargs={}
Call Stack:
File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
result = recursive_function(3)
Entering function: recursive_function
Arguments: args=(2,), kwargs={}
Call Stack:
File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
result = recursive_function(3)
File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
result = func(*args, **kwargs)
File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
return n * recursive_function(n - 1)
Entering function: recursive_function
Arguments: args=(1,), kwargs={}
Call Stack:
File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
result = recursive_function(3)
File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
result = func(*args, **kwargs)
File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
return n * recursive_function(n - 1)
File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
result = func(*args, **kwargs)
File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
return n * recursive_function(n - 1)
Exiting function: recursive_function
Return value: 1
Exiting function: recursive_function
Return value: 2
Exiting function: recursive_function
Return value: 6
==============================
Entering function: keyerror_function
Arguments: args=({'found': 1}, 'notfound'), kwargs={}
Call Stack:
File /Users/john/h/06/advanced_tracer_example.py, line 80, in <module>
keyerror_function(d, "notfound")
Exception in function keyerror_function:
Exception Stack Trace:
Traceback (most recent call last):
File "/Users/john/h/06/advanced_tracer_example.py", line 80, in <module>
keyerror_function(d, "notfound")
File "/Users/john/h/06/advanced_tracer_example.py", line 32, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/john/h/06/advanced_tracer_example.py", line 62, in keyerror_function
print(d[key])
~^^^^^
KeyError: 'notfound'
This implementation is particularly useful for:
- Debugging complex call hierarchies
- Understanding recursion paths
- Tracking down issues in large applications
- Performance profiling when combined with timing information
You can also extend this further by adding more parameters for different levels of detail or specific types of information you want to track.