2

I'm learning to use decorators at the moment but am struggling to wrap my head around their purpose and utility.

I initially thought they provided the convenient option to add extra functionality to an existing function (e.g. func()) without changing its source code, but if the additional functionality is executed whenever func() is called thereafter, then why wouldn't you just save the time/space/complexity and add the new functionality to func() directly?

E.g. Say I wanted to make a function print whenever it is executed, then wouldn't this:

def add(*args):
    out = sum(args)
    print("Function 'add' executed.")
    return out

create the exact same function as below, with far less code/complexity?

def log(func):
    def wrapper(*args, **kwargs):
        out = func(*args, **kwargs)
        print(f"Function '{func.__name__}' executed.")
        return out
    return wrapper

@log
def add(*args):
    return sum(args)

Off the top of my head, the only cases I can think of where the latter could potentially be preferable is if you're importing a generalised decorator to modify a function defined in a separate script, or are applying the same func to many functions so are saving space by just putting it in its own function (in which case it would seem more reasonable to just write a regular old function to simply execute it normally inside others).

EDIT

Perhaps a better way to formulate this question: Which of the following is preferable, and why?

def logger():
    # New functionality here
    return

def func1(*args):
    # func1 functionality
    logger()
    return

def func2(*args):
    # func2 functionality
    logger()
    return

Or

def logger(func):
    def wrapper(*args, **kwargs):
        out = func(*args, **kwargs)
        # New functionality here
        return out
    return wrapper

@logger
def func1(*args):
    # func1 functionality
    return

@logger
def func2(*args):
    # func2 functionality
    return
LDSwaby
  • 31
  • 4
  • Does this answer your question? [Python decorators just syntactic sugar?](https://stackoverflow.com/questions/12295974/python-decorators-just-syntactic-sugar) – noslenkwah Jan 04 '22 at 19:09
  • Sure, you _could_ do anything a decorator can do without that decorator, but at the expense of a lot more repetition. Not needing to repeat yourself / keeping code DRY is generally considered a good idea / valid reason to do things in a given way. – Charles Duffy Jan 04 '22 at 19:11
  • 1
    There's no point in a decorator that you only apply to one function. The "generalized" decorator that you can apply to many functions is indeed where they get useful. See for example `functools.lru_cache`, which is a decorator that automagically adds memoization to any function. – Samwise Jan 04 '22 at 19:11
  • 3
    Take a look at the [standard-library `functools`](https://docs.python.org/3/library/functools.html) module's collection of decorators. Do you really think it would be better if the standard library didn't include them? (And if you do... how do you distinguish this from an opinion, and perhaps a somewhat unpopular one that's certainly not held by anyone who _does_ use that library module?) – Charles Duffy Jan 04 '22 at 19:12
  • If you're concerned about efficiency, so my [answer](https://stackoverflow.com/questions/739654/how-to-make-function-decorators-and-chain-them-together/30283056#30283056) to the question [How to make function decorators and chain them together?](https://stackoverflow.com/questions/739654/how-to-make-function-decorators-and-chain-them-together) — specifically with respect to the idea of creating a *decorator factory*. – martineau Jan 04 '22 at 19:37
  • For your updated example, the chief advantage of the decorator is that it guarantees that the code in `logger` happens after the function has completed. If that's not important I'd say the decorator is unnecessary. – Samwise Jan 04 '22 at 22:10

3 Answers3

1

It promotes code reuse and separation of concerns.

To take your argument to the logical extreme, why use functions at all? Why not just have one giant main? A decorator is just a higher-order function and provides a lot of the same benefits that "traditional" functions do; they just solve a slightly different set of problems.

In your example, what if you wanted to change your log implementation to use the logging package instead of print? You would have to find every single function where you copy-pasted the logging behavior to change each implementation. Changing a single decorator's implementation would save you a lot of time making changes (and fixing bugs that arise from making those changes).

Decorators are typically used for behavior (in the decorator function) that wraps or modifies another set of behavior (in the decorated function). Some concrete examples could include:

  1. Start a timer when the decorated function starts, stop it when the function returns, and log the total runtime.
  2. Inspect the function's arguments, mutate some inputs, inject new arguments, or mutate the function's return value (see functools.cache).
  3. Catch and handle certain types of exceptions raised from inside the decorated function.
  4. Register the current function with some other object as a callback (see Flask).
  5. Run the decorated function within a temporary working directory, and clean up the directory when the function returns.

As many others have stated, you can do all of the above without decorators. But all of these cases could be made cleaner and easier to maintain with the help of decorators.

0x5453
  • 12,753
  • 1
  • 32
  • 61
  • Hey, thanks for thr response. But couldn't you do exactly the same by just creating a regular logging() function that you call in a one-liner in other functions? Surely this would give you more flexibility as you can call it at any point in the function rather than just before/after, as is the case with decorators. What's the advantage of using decorators over this method? – LDSwaby Jan 04 '22 at 20:49
  • @LDSwaby See my edit for some examples – 0x5453 Jan 04 '22 at 21:33
0

One benefit is minimizing clutter and repetition in your function implementations. Consider:

@log
def add(*args):
    return sum(args)


@log
def mult(*args):
    return math.product(*args)

vs:

def add(*args):
    out = sum(args)
    print("Function add executed.")
    return out


def mult(*args):
    out = math.product(*args)
    print("Function mult executed.")
    return out

and imagine that repetition over, say, a hundred functions in a large codebase.

If you kept the log function and used it without decorator syntax you might have something like:

def _add(*args):
    return sum(args)


def _mult(*args):
    return math.product(*args)


add = log(_add)
mult = log(_mult)

which isn't the worst thing in the world, but it'd be annoying for a reader having to bounce through a level of indirection each time they try to look up a function's implementation.

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • 1
    Hey, cheers for the helpful reponse. But couldn't you reduce the clutter/repetition by roughly the same amount by just defining a regular log() function (i.e. not a decorator) and just executing it normally inside the new functions (with the added benefit of being able to execute it anywhere in the function rather than just before or after)? Is the only advantage of decorators over this method just marginally more readable code? – LDSwaby Jan 04 '22 at 19:34
  • In your particular case, yeah. If you wanted your logging to do something before *and* after (like, say, tracking the execution time) it gets more complicated -- then you want a context manager (which adds an extra level of indentation to every function, which is kinda gross). If it needs to transform the return value and/or arguments you have other types of complexity to manage. These are pretty common use cases that decorators are able to just abstract away into a single line outside the body of the function. – Samwise Jan 04 '22 at 21:58
  • Long way of saying, you don't need to use decorators for everything. It sounds like you haven't stumbled across a good use case for them yet, but they are out there. Logging is actually one of the cases I've found them *most* useful for, but as alluded to in my last comment, I was doing a lot more in my logging decorators -- timing, aggregation, etc. Implementing all that across every function without decorators would have been unpleasant. – Samwise Jan 04 '22 at 22:05
  • I'm with ya - thanks a lot :) – LDSwaby Jan 18 '22 at 19:27
0

The most important factor to the benefit of decorators is the DRY code principle

"Do Not Repeat Yourself" as an principle lends itself to creating easy-to-understand, easy-to-write code. Python's decorators are a fantastic example of features that minimise unnecessary code repetition:

Consider the @dataclass decorator. In short, it allows for classes which store only instance attributes to be written easier, as shown in the following example:

class Person:
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

versus

@dataclass
class Person:
    name: str
    age: int
    gender: str

The important idea about decorators to realise, however, is that creating the @dataclass decorator and writing the second (better) implementation of Person DO INDEED take more time than just writing the first Person implementation.

However, very critically, the difference emerges when you write a second, or third data-oriented class! At this point, by writing the @dataclass decorator, the creation of every single class can be sped up, by removing the boilerplate.

This example generalises to all decorators: writing a @log decorator is slow for one function, but worth it to log 100 different functions.

  • "creating the @dataclass decorator and writing the second (better) implementation of Person DO INDEED take more time than just writing the first Person implementation" - I would have to disagree. In the first impl, I'd have to write out an `__init__` and arguments to the method, and also repeat the attribute names in the body of the method. I feel it's actually a little easier to use a dataclass approach at least in this example. – rv.kvetch Jan 05 '22 at 15:29
  • 1
    @rv.kvetch I was refering more to the idea of personally implementing both of them, meaning that you would have to implement [@dataclass (link to docs.python)](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) yourself. This is somewhat more like what OP was asking, since that is what they found to be so wasteful. I worry my wording isnt very clear. – Seliksander Jan 05 '22 at 15:46