Turn Your Python Function into a Decorator with One Line of Code
A new way to write shorter, clearer, and more readable Python decorators that also act as a context manager
Do you want to write a decorator but can’t remember the syntax? Decorators have a pretty difficult syntax that involves a lot of boilerplate code. In this article we’ll demonstrate a simpler way of writing decorators. This new way will be much shorter, clearer, and more readable. Let’s code!
The default way to create a decorator
The code below is the default way to create a decorator. It times how long a decorated function runs. Check out this article for a deep dive in decorators.
def timer(name:str) -> Callable:
def decorator(func:Callable) -> Callable:
@wraps(func)
def decorator_implementation(*args, **kwargs):
try:
print(f"TIMER: {name} start")
strt = time.perf_counter()
return func(*args, **kwargs)
finally:
print(f"TIMER: {name} finished in {time.perf_counter() - strt}")
return decorator_implementation
return decorator
This way we can use our code like this:
@timer(name="test")
def my_func(name:str, age:int) -> str:
return f"{name} is {age} years old"
my_func(name="mike", age=34)
# TIMER: test start
# mike is 34 years old
# TIMER: test finished in 5.299999999998532e-06
What’s the problem with this approach?
Personally I never remember how to write a decorator and always have to copy-paste code from old projects. I think this is because of the rather difficult-to-read syntax which involves 3 nested functions which make the decorator much harder to understand. Let’s find out how this simplify.
An easier way to write a decorator
The implementation below does the exact same thing as the decorator in the previous part with only one function. It’s much more readable and we only need to add the @contextmanager
decorator and convert the function to a generator by yield
ing.
@contextmanager
def timer(name:str) -> Generator:
try:
print(f"TIMER: {name} start")
strt = time.perf_counter()
yield
finally:
print(f"TIMER: {name} finished in {time.perf_counter() - strt}")
All in all we can use the function in the exact same way:
@timer(name="AS DEC")
def my_func(name:str, age:int) -> str:
return f"{name} is {age} years old"
my_func(name="mike", age=34)
# TIMER: AS DEC start
# mike is 34 years old
# TIMER: AS DEC finished in 5.399999999995686e-06
Functionality as a context manager
Personally I find the new function more readable and understandable. We need a few simple changes but they offer a lot of functionality in return. Decorating a function gets much easier and we can even use the decorator function (timer
in the example above) as both a decorator and a context manager:
with timer(name='ctx_manager'):
print("In context-manager")
# TIMER: ctx_manager start
# In context-manager
# TIMER: ctx_manager finished in 4.200000000002813e-06How is this possible?
Combining decorator and context manager
We can even use the same decorator function as both a decorator and a context-manager in the same block:
with timer(name='as ctx'):
fn_with_ctx_decorator(name="john", age=42)
print("In context-manager")
# TIMER: as ctx start
# TIMER: AS DEC start
# john is 42 years old
# TIMER: AS DEC finished in 3.7000000000023126e-06
# In context-manager
# TIMER: as ctx finished in 3.0000000000002247e-05
How does this work?
Under the hood, contextlib
takes care of our decorator-function by using @contextmanager
to wrap it in an object that act as both a decorator and a context-manager. The way this works is pretty technical, very interesting and requires an article of its own. Follow me to stay tuned!
For the scope of this article it’s enough to know that contextlib
uses the @contextmanager
decorator to convert out decorator-function into an object that can be used as both a decorator and contextmanager.
Downsides
Although the new decorator offers us a lot of functionalities , there are some downsides. The most prominent one is that within our decorator-function, we have no access to the function that we’re actually decorating. We also have no access to its args and kwargs. This prevents us from modifying these variables but in my opinion this is something you should rarely do.
The second downside is that the decorator-function must be a generator-function. This means that it must yield
somewhere in its body. This may force you to rewrite a bit of your code.
Conclusion
With @contextmanager
writing a decorator is easy and readable. It takes care of a lot of boilerplate and even acts as a contextmanager. The downside is that for all this automation and “magic under the hood”, you lose some control in the form of access to your function and arguments.
The way contextlib
works under the hood is pretty complex and deserves an article on its own, so follow me if you’re interested!
I hope this article was as clear as I hope it to be but if this is not the case please let me know what I can do to clarify further. In the meantime, check out my other articles on all kinds of programming-related topics.
Happy coding!
— Mike
P.S: like what I’m doing? Follow me: