The @decorator
syntax in Python is easy to abuse. After all, it's simply a syntactic sugar for:
obj = decorator(obj)
The obj
must be a function or a class but Python doesn't care about the output value that is then assigned to the same name. It may be, quite literally, anything assignable. A perfect place to get clever and show to your fellow programmers how cool you are!
Rule of thumb
This is of course very wrong, especially in Python, where the principle of least surprise is universally respected. When deciding if something can be implemented as a decorator my rule of thumb is that a decorator should not alter the semantics of its argument function.
Or, put without the use of the curse word "semantics", a decorated function is expected to be called in exactly the same way — arguments, the return value and general meaning — as if it wasn't decorated. This way the user can still see how to use the decorated function by looking only at its code (or docs) without hunting for the definition of the decorator.
Bad example
I once saw a code where decorators were used as a sort of a weird DSL:
@text
@user
def create_post(user, text):
backend.callCreatePost(user, text)
...
create_post(request) # wait, "request"? wtf?
All the interface functions were called in a similar fashion with a dict-like argument from which it was possible to get everything they needed. The idea was to extract common actions, like getting a user object, into decorators and let the resulting function look "cleaner".
As you might expect, it was really hard to debug. And since all those decorators were inter-dependant on each other providing parameters in the correct places you would periodically fight problems like "we need this parameter before that one but can't do it because that other function already wants them the other way around". Much time was also spent on solving really interesting problems like introspecting the name of a n-th positional argument which wasn't even possible on Python 2.4 back then…
A simpler approach, while using more characters, would be nonetheless more readable:
def create_post(request):
user = get_user(request)
text = get_text(request)
backend.callCreatePost(user, text)
A side-note
Also keep in mind that when you apply a @decorator
to a function the function itself is effectively lost from the namespace and there's no (simple) way to call it in its original un-decorated form. This might be a problem, especially in a reusable library-style code because it's usually hard to anticipate at the time of writing all the ways in which it can be used in the future. Sometimes it's best to provide both the function and the decorator separately and let the user decide how to call them.
Comments: 13 (noteworthy: 1)
re: your sidenote - you can use functools.wraps (http://docs.python.org/library/functools.html#functools.wraps) in order to preserve the decorated function namespace.
Michael, thanks, I didn't know
wraps
keeps the original function in the.func
attribute of the decorator.Or wait… I tried this:
… and
func.func
doesn't exist. What did you mean then?You can check the source code for wraps here to get the idea of what it does:
http://hg.python.org/cpython/file/4f891f44ec15/Lib/functools.py#l12
The source didn't make it clear about what you (and Michael) mean :-). I still suspect we're talking about different things here. I was saying about accessing the original function from the surrounding namespace, Michael was saying about preserving the namespace of the function.
In other words, given the example in my previous comment, how to access the original
func()
that when called would not stick the word "decorated" to the result?Oh, I wasn't supporting Michael's words. I was trying to show that all
wraps
does is preserving__module__
,__name__
and__doc__
as well as__dict__
. None of those give you an access to the original function, so the answer to your question is you don't, as far as I can see.OK, got it. This is exactly what I meant in my side note :-).
Hey-hey, how about docs for decorator? ;)
In any case, your example makes sense as an example of abusing decorator syntax and crappy API at the same time. But then you could make a really bad hierarchy of classes and abuse them to death with metaclasses. And it could be nice and useful. Or terminally bad. Does it mean we shouldn't write and use metaclasses which change semantics of classes completely?
I.e. Django forms or models - you define properties, but then they are totally gone from your class. But then suddenly they are here on your instance. Isn't this like a decorator, which changes semantics of a function? Isn't then decorators changing semantics justificable?
I believe they are not inherently bad, they are bad only when you do crappy stuff with them. Which is always bad, but it's not like there are simple rules to follow to make people not do crappy things.
Noteworthy comment
In 3.2+,functools.wraps adds a "wrapped" attribute to bypass the wrapping decorator (this was driven by the introduction of lru_cache).
More generally, while it needs to be used with restraint, it's entirely appropriate for decorators to be semantically significant. Consider @classmethod, @staticmethod, @property and @contextmanager.
There's useful lib for doing all fancy things in a clean way with decorators - venusian http://docs.pylonsproject.org/projects/venusian/en/latest/ by @chrism - it simplifies creation of decorators which only attach some callback (which can be deferred by the way) to function thus not modifying original function at all.
piranha:
This is true about any rule of thumb: you can break it if you understand implications. But these "rules" do help in the process of this judgment anyway.
I think this article was inspired by that comment: http://vorushin.ru/blog/26-decorators-python/#comment-248848219
When I first time read this comment I remember it for a long time)
Did you know that knowledge base software from Website Scripts is developed using decorators with Python