10

I've been learning and experimenting with decorators. I understand what they do: they allow you to write modular code by allowing you to add functionality to existing functions without changing them.

I found a great thread which really helped me learn how to do it by explaining all the ins and outs here: How to make a chain of function decorators?

But what is missing from this thread (and from other resources I've looked at) is WHY do we need the decorator syntax? Why do I need nested functions to create a decorator? Why can't I just take an existing function, write another one with some extra functionality, and feed the first into the 2nd to make it do something else?

Is it just because this is the convention? What am I missing? I assume my inexperience is showing here.

martineau
  • 119,623
  • 25
  • 170
  • 301
Erich Purpur
  • 1,337
  • 3
  • 14
  • 34
  • Because then for each combination of the decorator functions you have you will have to write another decorator function. Compositing decorator functions are the abstraction so you don't have to manually write those combinations yourself. – Netwave Oct 01 '18 at 14:47
  • Package a decorator (or multiple), and you can easily apply all that functionality to other packages, without having to write new functions in those other packages: just one extra line for the relevant functions. – 9769953 Oct 01 '18 at 14:49
  • And yes, technically you can precisely do what you suggest. But it makes things more cumbersome, and pollutes the code with extra functions. A decorator with clear functionality (from its name) makes code more readable, instead of having to read one function, only to then look up the other function. – 9769953 Oct 01 '18 at 14:50
  • 1
    I'm not exactly an expert on design patterns, but I think [Python decorators](https://realpython.com/primer-on-python-decorators/) are not the same as the decorator pattern. (Maybe they are related at an abstract level, but the dup-targets do not seem to be practically relevant for this question.) – MB-F Oct 01 '18 at 14:58
  • Decorators are very common when the function modification is temporary. Consider the use of decorators in adding testing or timing functions. – Michael Molter Oct 01 '18 at 15:01

3 Answers3

6

I will try to keep it simple and explain it with examples. One of the things that I often do is measure the time taken by API's that I build and then publish them on AWS.

Since it is a very common use case I have created a decorator for it.

def log_latency():
def actual_decorator(f):
    @wraps(f)
    def wrapped_f(*args, **kwargs):
        t0 = time()
        r = f(*args, **kwargs)
        t1 = time()
        async_daemon_execute(public_metric, t1 - t0, log_key)
        return r
    return wrapped_f

return actual_decorator

Now if there is any method that I want to measure the latency, I just put annotate it with the required decorator.

@log_latency()
def batch_job_api(param):
    pass

Suppose you want to write a secure API which only works if you send a header with a particular value then you can use a decorator for it.

def secure(f):
@wraps(f)
def wrapper(*args, **kw):
    try:
        token = request.headers.get("My_Secret_Token")
        if not token or token != "My_Secret_Text":
            raise AccessDenied("Required headers missing")
    return f(*args, **kw)
return wrapper

Now just write

@secure
def my_secure_api():
    pass

I have also been using the above syntax for API specific exceptions, and if a method needs a database interaction instead of acquiring a connection I use @session decorator which tells that this method will use a database connection and you don't need to handle one yourself.

I could have obviously avoided it, by writing a function that checks for the header or prints time taken by API on AWS, but that just looks a bit ugly and nonintuitive.

There is no convention for this, at least I am not aware of them but it definitely makes the code more readable and easier to manage. Most of the IDE's also have different color syntax for annotation which makes it easier to understand and organize code.

So, I would it may just be because of inexperience; if you start using them you will automatically start knowing where to use them.

hassan_ashraf
  • 305
  • 1
  • 2
  • 7
3

From PEP 318 -- Decorators for Functions and Methods (with my own added emphasis):

Motivation

The current method of applying a transformation to a function or method places the actual transformation after the function body. For large functions this separates a key component of the function's behavior from the definition of the rest of the function's external interface. For example:

def foo(self):
    perform method operation 
foo = classmethod(foo) 

This becomes less readable with longer methods. It also seems less than pythonic to name the function three times for what is conceptually a single declaration. A solution to this problem is to move the transformation of the method closer to the method's own declaration. The intent of the new syntax is to replace

def foo(cls):
    pass 
foo = synchronized(lock)(foo) 
foo = classmethod(foo)

with an alternative that places the decoration in the function's declaration:

@classmethod
@synchronized(lock)
def foo(cls):
    pass
Steven Rumbalski
  • 44,786
  • 9
  • 89
  • 119
3

A great example of why decorators are useful is the numba package.

It provides a decorator to speed up Python functions:

@jit
def my_slow_function():
    # ...

The @jit decorator does some very amazing and complex operations - it compiles the whole function (well, parts of it) to machine code. If Python did not have decorators you would have to write the function in machine code yourself... or more seriously, the syntax would look like this:

def my_slow_function():
    # ...

my_slow_function = jit(my_slow_function)

The purpose of the decorator syntax is to make the second example nicer. It's just syntactic sugar so you don't have to type the function name three times.

MB-F
  • 22,770
  • 4
  • 61
  • 116