The Day I Stopped Repeating Myself in Python
How I learned to stop copying code and love decorators
For years, I was a code copier. A control-C, control-V programmer, stuck in a loop of repetition.
I'd build a function, and then I'd build another one that did something similar. Maybe I needed to time how long a function ran, or maybe I needed to log its inputs and outputs. So I'd copy a few lines of code from a previous project, paste them into my new function, and hope for the best.
It wasn't elegant. It wasn't smart. It was a messy, repetitive habit that made my code fragile and hard to manage. Every time I needed to change my logging format or my timing method, I had to hunt down and update the code in every single function. I was violating the most fundamental rule of software engineering: Don't Repeat Yourself (DRY).
Then I discovered decorators.
You’ve probably encountered the @
symbol in Python code, hovering like a magical sigil over a function. I used to think it was some kind of arcane wizardry, reserved for advanced libraries and frameworks. It turns out, it's just a beautiful and powerful solution to my code-copying problem.
A decorator is a function that takes another function and extends its behavior without permanently altering it. It's like putting your code in a custom-made, reusable box. The logic inside the box is still the same, but you can add new features and protections on the outside.
The Code Behind the Magic
To really understand decorators, you have to get one key concept: functions in Python are just objects.
This is huge. It means you can treat a function just like an integer or a string. You can pass it to another function, return it from a function, or assign it to a variable. This simple fact is the foundation of every decorator.
Let's look at the problem again, this time with our newfound knowledge. We want to add timing to a simple greet
function.
import time
def greet(name):
time.sleep(1)
print(f"Hello, {name}!")
# We want to time this function without touching its code.
Instead of copying and pasting the timing code, let's write a function that takes greet
as an argument and adds a timer to it.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
# ... inside wrapper ...
result = func(*args, **kwargs) # Call the original function
end = time.time()
print(f"'{func.__name__}' ran in {end - start:.2f} seconds.")
return result
return wrapper
# Now, let's manually "decorate" our greet function
greet = timer(greet)
# This single line of code is the key. It replaces the original `greet`
# function with the new `wrapper` function that contains our timing logic.
greet("Jamie")
Hello, Jamie!
'greet' ran in 1.00 seconds.
If you understand that code block, you understand the core of how decorators work. The timer
function is our decorator. It's not magic; it's just a function that returns a new function.
The Clean and Elegant @
Syntax
This manual process is a little clunky. Thankfully, Python offers a cleaner way to do the exact same thing with the @
syntax.
import time
import functools # Provides tools for working with functions
def timer(func):
@functools.wraps(func) # This is the "good citizen" step
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"'{func.__name__}' ran in {end - start:.2f} seconds.")
return result
return wrapper
@timer # This applies our decorator!
def greet(name):
"""A simple function that greets a person."""
time.sleep(1)
print(f"Hello, {name}!")
# The function is now automatically decorated.
greet("Chris")
print(f"Function name: {greet.__name__}")
print(f"Function docstring: {greet.__doc__}")
Output:
Hello, Chris!
'greet' ran in 1.00 seconds.
Function name: greet
Function docstring: A simple function that greets a person.
Notice the new line, @functools.wraps(func)
. This is a crucial step for being a good decorator citizen. It copies the original function's name, docstring, and other metadata to the wrapper. Without it, debugging tools would see the function as 'wrapper'
, making your code harder to work with.
Where Decorators Shine
This is more than a party trick. Decorators are everywhere, solving real-world problems by keeping code clean and reusable. The same pattern we used for timing works for any cross-cutting concern you want to add to functions.
- Logging: Automatically log every function call, its arguments, and return value. Incredible for debugging.
- Authentication: In web frameworks like Flask, use
@login_required
to protect routes. - Permissions: Check user permissions (e.g.,
@admin_only
) before executing a function. - Caching: Store function results so repeated calls with the same arguments are lightning fast.
You don't need to be a wizard to use them. While decorators can get more complex, mastering the basics we've covered here handles the vast majority of real-world use cases.
The key is to recognize repetitive "boilerplate" code in your functions—the kind of logic you find yourself copying and pasting. That is a perfect candidate for a decorator. By extracting that logic into a reusable function, you'll be writing cleaner, more professional, and more maintainable code in no time.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Comments
Post a Comment