It's absolutely baffling that an article called demystifying decorators does not actually describe decorators at all, preferring to keep them a mystery. Decorators are nothing more than a special syntax for calling a function, the meat of which can easily be explained in a single paragraph including a supporting example.
I think much of the challenge people have with decorators is not in understanding how they work, but in identifying when they are useful.
I wonder if the people that struggle with decorators have a fundamental struggle with seeing functions as objects that can be redefined or modified.
A decorator basically just does something when a function is defined. They can be used to do something when a function is defined (such as register the function with a list of functions), or modify the function's behavior to do something before and/or after the function is called (like create a poor man's profiling by timing the function call).
You can do all that without decorators, of course, but the code looks a lot cleaner with them.
That's a long read, but it is an awkward thing tbh.
My take on it is that, if you're going to write a decorator, make sure you test it thoroughly and be sure to cover the ways that it will actually be used.
To properly handle a decorator that needed to be called both with and without arguments, I ended up writing a function decorator that returned a wrapped function when there were no parameters provided for the decorator (*args param to the decorator function undefined) and, otherwise, returned an instance of a class that defined __call__ and __init__ methods.
I'm still a bit surprised that I had to do it that way, but that's how it shook out.
Is there any good resource about class decorators specifically? It is more common to see function decorators in the wild, so my knowledge about the class ones is negligible at best
outside of __init__, I thought it wasn't the pythonic way to directly invoke double underscore functions like closure? I recall reading that you should implement a class that overrides/implements such functions instead? I get why the author might be doing it this way to describe the topic of the post, but would calling __closure__() be acceptable in production code?
I'm not sure who this article is for, but I think it doesn't strike at the heart of the topic.
Half of the article is devoted to closures, but closures aren't essential for decorators. And the __closure__ attribute is an implementation detail that is really irrelevant. (For comparison, JavaScript has closures just like Python, but it doesn't expose the closed-over variables explicitly the way Python does.)
Decorators are simply higher order functions that are used to wrap functions. The syntax is a little funky, but all you need to know is that code like:
@foo
@bar
def baz(args):
return quux(args)
Is essentially equivalent to: baz = foo(bar(lambda args: quux(args))
...i.e. decorators are functions that take a callable argument and return a new callable (which typically does something and then calls the argument function--or not, as the case may be).Then there are seemingly more complex expressions like:
@foo(42, 'blub')
def bar(..): ...
This looks like a special kind of decorator that takes arguments, but it looks more complex than it really is. Just like `foo` was an expression referencing a function `foo(42, 'blub')` is just a regular Python function call expression. That function call should then itself return a function, which takes a function argument to wrap the function being decorated. Okay, I admit that sounds pretty complex when I write it out like that, but if you implement it, it's again pretty simple: def foo(i, s):
def decorator(f):
def invoke(*args, **kwargs):
print('decorator', i, s)
f(*args, **kwargs)
print('done')
return invoke
return decorator
@foo(42, 'blub')
def hello():
print('Hello, world!')
hello()
# prints:
# decorator 42 blub
# Hello, world!
# done
This is an extra level of indirection but fundamentally still the same principle as without any arguments.And yes, these examples use closures, which are very convenient when implementing decorators. But they aren't essential. It's perfectly possible to declare a decorator this way:
class Invoker:
def __init__(self, f):
self.f = f
def __call__(self):
print('before call')
self.f()
print('after call')
def decorator(f):
return Invoker(f)
@decorator
def hello():
print('Hello, world!')
hello()
# prints:
# before call
# Hello, world!
# after call
It's the same thing but now there are no closures whatsoever involved.The key point in all these examples is that functions in Python are first-class objects that can be referenced by value, invoked dynamically, passed as arguments to functions, and returned from functions. Once you understand that, it's pretty clear that a decorator is simply a wrapper that takes a function argument and returns a new function to replace it, usually adding some behavior around the original function.
One thing I've learned writing documentation (and struggling) is that, when you find yourself writing at length, splitting text into sections, and peppering a copious amount of examples, you need to stop and do 2 things:
- Audit for conciseness
- Add a tl;dr summary at the top
And I do wish this had a real example it was playing with, and it consistently used that same example all the way through as it built the layers of what the problem being solved with decorators is. It's a lot easier to teach a concept when you can contextualize it in a way that's shows it's usefulness.
I'm not sure if this metric really matters, but this is wordier than the PEP that describes decorators.
Decorators in Python are not bad at all.
Annotations in Java, on the other hand, which have the same syntax, are a monument to all our sins.
Jdjfm df
I have decidedly mixed feelings on Python decorators, but tend to shy away from them. The parts I like are that they can be a good way to reuse certain functions and they make surface level code much more concise and readable. The trouble comes when you need to delve into a stack of decorators to try to untangle some logic. Bouncing between decorators feels harder to track than a set of functions or classes.
One project I worked on used decorators heavily. Things worked great on the happy path but trying to track a subtle bug happening in a function with nine complex decorators is not a fun experience.