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) (feed)

  1. Michael Warkentin

    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.

  2. Ivan Sagalaev

    Michael, thanks, I didn't know wraps keeps the original function in the .func attribute of the decorator.

  3. Ivan Sagalaev

    Or wait… I tried this:

    def decorator(f):
        @wraps(f)
        def wrapper(value):
            return 'decorated %s' % f(value)
        return wrapper
    
    @decorator
    def func(value):
        return 'process %s' % value
    

    … and func.func doesn't exist. What did you mean then?

  4. dm

    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

  5. Ivan Sagalaev

    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?

  6. dm

    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.

  7. Ivan Sagalaev

    OK, got it. This is exactly what I meant in my side note :-).

  8. piranha

    how to use the decorated function by looking only at its code (or docs) without hunting for the definition of the decorator.

    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.

  9. ncoghlan_dev

    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.

  10. Andrey Popp

    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.

  11. Ivan Sagalaev

    piranha:

    I believe they are not inherently bad, they are bad only when you do crappy stuff with them.

    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.

  12. seriyPS

    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)

  13. Sebastian Rockefeller

    Did you know that knowledge base software from Website Scripts is developed using decorators with Python

Add comment

Format with markdown