pythonpython-decorators

Having trouble seeing the motive for decorators in *simple* examples


I've surfed some tutorials online about decorators. I'm having trouble seeing their benefit for simple examples. Here is a common example of a function crying out to be decorated, taken from this page:

# Unelegant decoration
#---------------------
def make_pretty(func):

    def inner():
        print("I got decorated")
        func()
    return inner

def ordinary():
    print("I am ordinary")

decorated_func = make_pretty(ordinary)
decorated_func()
    # Output
    #-------
    # I got decorated
    # I am ordinary

The benefit of decorators is described as follows: "Instead of assigning the function call to a variable, Python provides a much more elegant way to achieve this functionality".

# Elegant decoration
#-------------------
@make_pretty
def ordinary():
    print("I am ordinary")

ordinary()
    # Output
    #-------
    # I got decorated
    # I am ordinary

Other tutorials provide similar examples and similar motivations, e.g., here.

The difficulty I have with this explanation is that it doesn't quite fit the intent of adding functionality to the function to be decorated without modifying it (another frequent explanation of decorators). In the above example, undecorated "ordinary()" is no longer available, so decorating it does not in fact leave the original function available for use in situations where the decoration is not needed or desired.

The other more specific motive is greater elegance by "not assigning the function call to a variable". For the "Unelegant decoration" code, however, this is easily achieved without the "Elegant decoration" code above:

make_pretty(ordinary)()
    # Output
    #-------
    # I got decorated
    # I am ordinary

The tutorials typically proceed by describing decorators in cases where functions take arguments. I can't follow the motive for them because I can't even understand the benefit in the simplest case above. SO Q&A's also talk about practical use cases (e.g., here), but it's hard to follow the reason for decorators, again when the motive in the simplest case above isn't clear.

Is it possible to state in plain language what the benefit is in the simplest case above, with no function arguments? Or is the benefit only going to be clear by somehow figuring out the more complicated cases?


Solution

  • The point of decorators is similar to most other functions: modularity and "don't repeat yourself".

    If you have a bunch of functions that should all print certain messages before and after they do their real work, you could put print() calls in each of them. But that's tedious and repetitive. So instead, you define a decorator that you use when defining each of these functions, it will add those print() calls to all of them automatically.

    This also provides modularity. If you want to change the messages that are printed, you just do it in one place, the decorator, rather than having to edit all the functions.

    Many decorators are significantly more complex than the toy example you show. Consider the built-in @property decorator used when defining properties in classes, it turns the named method into a property that can be used like an attribute. Or @functools.cache that adds automatic caching to the function you're defining. Decorators in libraries like these make it easy to add features to functions you're writing.

    Note that decorators don't always have to augment the behavior of the function. They can leave the function's behavior unchanged, but do some additional processing. For instance, in Flask or discord.py, you use decorators to define webserver routes and discord commands. These add the decorated function to a routing table used by the libraries when processing incoming messages, but you can also call them directly as well.