This article originated from personal public account: TechFlow, original is not easy, for attention


Today, in the 12th installment of our Python series, we take a look at the Python decorator.

A fart joke

I learned its importance when I interviewed for a job almost five years ago. At that time, I was just learning Python. After reading Liao Xuefeng’s blog, I went to the interview. I am not applying for a Python development position, but JD says that I need to be familiar with Python. I read a blog on the Internet that often asks about decorators when it comes to Python, and I thought, I’ve already seen decorators, it shouldn’t be a problem…

The interviewer asked me what a Python decorator is. Due to nervousness and forgetting, I hummed and hawed for a long time and did not answer. I vaguely heard a sigh on the other end of the phone…

Years later, I can’t remember what company it was (it’s probably not that big), but the fact that decorators are important left a deep impression on me.

Nature of decorator

Now, if another interviewer asks me what a decorator in Python is, I can answer it in one sentence. It’s not that I’m being pusillanimous, and it really only takes one sentence. A decorator in Python is essentially a higher-order function.

You may not know the definition of higher-order functions, but that’s ok, we can do the analogy. In mathematics a higher derivative, like a second derivative, is the derivative of a derivative. So a higher-order function is naturally a function of a function, combined with functional programming that we talked about earlier, that is, a function that returns a function. But this definition is sufficiently unnecessary to say that decorators are higher-order functions, but higher-order functions are not all decorators. Decorators are a special use of higher-order functions.

Any parameters

Before we get into the actual use of decorators, let’s get familiar with arbitrary arguments in Python.

Python supports arbitrary arguments, which are written as *args, **kw. The meaning of the expression is to accept any form of argument.

For example, let’s define a function:

def exp(a, b, c='3', d='f'):

    print(a, b, c, d)

Copy the code

We can call it like this:

args = [1.3]

dt = {'c'4.'d'5}



exp(*args, **dt)

Copy the code

The final output is 1, 3, 4, 5. That means we can represent any parameter with a list and dict. Python requires mandatory arguments to be written before optional arguments, and mandatory arguments are not identified by their names. That is, you can pass 1 instead of a=1. Optional parameters that are not named can be represented as a list, while optional parameters must be named. These parameters can be represented by dict, and the two can be added together to represent any type of parameter.

Notice that we pass in lists and dict preceded by * and ** to expand all the values in list and dict. If not, the list and dict are passed as a whole.

So if a function is written like this, it means it can take any kind of argument.

def exp(*args, **kw):

    pass

Copy the code

Define decorator

Once you understand how to write arbitrary parameters, decorators are easy.

Since we can accept any argument with *args, **kw. Python also supports passing one function as an argument to another function. If we pass a function and all of its arguments to another function, then we can implement the proxy.

As in the previous example, we add an additional function:



def exp(a, b, c='3', d='f'):

    print(a, b, c, d)



def agent(func, *args, **kwargs):

    func(*args, **kwargs)



args = [1]

dt = {'b'1.'c'4.'d'5}



agent(exp, *args, **dt)

Copy the code

The essence of decorator is such an Agent function, but if you need to manually pass in when using it, it will be very troublesome and not very convenient to use. So there are specific libraries in Python that we can use to annotate decorators to greatly simplify operations:

from functools import wraps



def wrapexp(func):

    def wrapper(*args, **kwargs):

        print('this is a wrapper')

        func(*args, **kwargs)

    return wrapper





@wrapexp

def exp(a, b, c='3', d='f'):

    print(a, b, c, d)





args = [1.3]

dt = {'c'4.'d'5}



exp(*args, **dt)

Copy the code

In this example, we define a wrAPexp decorator. We implement the decorator logic in the Wrapper method, where the argument func passed in wrapexp is a function and the arguments in wrapper are arguments to func. So we call func(*args, **kw) inside the Wrapper, calling the annotated function itself. For example, in this case, we did nothing but print an extra line ‘this is a wrapper’ before the original call to indicate that our decorator call was successful.

Decoration purpose

Once we understand the basics of how a decorator works, we naturally ask a natural question: What does it really do?

If you don’t appreciate the power of decorators from the example above, let me give you another hint. For example, if you are a programmer, you have worked hard to make a feature, written thousands of lines of code, hundreds of functions, and finally passed the review online. At this time, your product manager came to you and said, after analysis, we found that the running speed of the online function was not up to standard, and there were often requests timed out. Could you please calculate the running time of each function, so that we could find out where we need to optimize?

This is a perfectly reasonable request, but think about how much code you would write if you had to manually add a time calculation to each of the hundreds of functions you wrote. If you accidentally change a function, you have to check each one, and if you are strict you have to write a unit test for each one…

I think normal programmers would resist this requirement.

But with a decorator, it’s easy. We can implement a decorator that calculates the time of a function, and then we just need to annotate each function.

import time

from functools import wraps

def timethis(func):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = func(*args, **kwargs)

        end = time.time()

        print(func.__name__, end-start)

        return result

    return wrapper

Copy the code

This is where decorators are most useful, wrapping extra functionality around a function without modifying its internal code.

Meta information

We said that decorators are higher-order functions in nature, so we can call decorators as higher-order functions, such as the following:

def exp(a, b, c='3', d='f'):

    print(a, b, c, d)





args = [1.3]

dt = {'c'4.'d'5}



f = wrapexp(exp)

f(*args, **dt)

Copy the code

The result is the same as using an annotation, which means that we are essentially calling the decorator to return a new function.

Since it is the same as the higher-order function, there is a problem. Instead of using the original function, we are using a new function returned by the decorator. Although this function has the same function as the original function, some basic information has been lost.

For example, we can print out the name of the function to do an experiment:


The normal function call __name__ returns the name of the function, but this changes when we add the decorator annotation. Again, we print the result of the decorator annotation:


We will see that the output is wrapper because the function inside the decorator we implemented is called wrapper. Not only __name__, but also a lot of other basic information inside functions, such as __doc__, __annotations__, and so on, that is called meta information, and this meta information is lost because we use annotations.

Is there any way to preserve meta-information about these functions?

In fact, Python provides a special decorator to preserve the meta information of the function. All we need to do is add a wraps annotation to the Wrapper function that implements the decorator.

def wrapexp(func):

@wraps(func)

    def wrapper(*args, **kwargs):

        print('this is a wrapper')

        func(*args, **kwargs)

    return wrapper

Copy the code

With this annotation in place, we can examine the function’s meta information and see that it is as expected.


conclusion

Now that you know about decorators in Python, if you look at the @property, @StaticMethod annotations we’ve used before, you can see that the implementation behind them is also a decorator. Flexible use of decorators can greatly simplify our code, make our code more formal and concise, and have the flexibility to implement some special functions.

There are many uses for decorators. This is just the most basic of them, and I will share more of them in a future article. As I said at the beginning of this article, decorators are one of the skills you must learn to advance Python. Want to master this language, flexible use, understand the big guy’s source code, decoration is a must.

Hope everyone can harvest, original is not easy, brazen beg a praise and attention ~