Everything in Python is an object, and functions are objects. Functions can be assigned to a variable, functions can be passed as arguments to another function, and functions can return functions via a return statement. A decorator is a function that accepts functions and returns functions. This may sound a little convolutious at first, but a decorator is essentially a function. Decorators are designed to enhance code robustness through sectional-oriented programming, such as logging, caching, and permissions verification. Let’s take a step-by-step look at the use of decorators in Python.

Let’s start with a simple function definition that has only one function: Hello World:

def hello() :
    print('Hello World! ')
Copy the code

Now we have a new requirement to add logging before the old function is executed, so we have the following code:

def hello() :
    print('run hello')
    print('Hello World! ')
Copy the code

Now that the above problem is solved, all you need to do is add a line of code. The problem is, in a real work scenario, we might not only need to modify one Hello function, but also 10 or 20 functions that need to add logging. The problem is that it is not possible to copy one line of code from one function to the next, and it is possible to add not just one line, but hundreds of lines. And this can lead to a lot of duplicate code, when there is too much duplicate code, you need to be careful, it is easy to cause unexpected bugs, and difficult to troubleshoot and maintain. An easy way to do this would be to define a function that prints logs, and then call the log function in each function:

def log() :
    print('run hello')

def hello() :
    log()
    print('Hello World! ')
Copy the code

This still requires fixing the code inside the Hello function. Not that you can’t do this, but it clearly violates the open-closed principle of being closed to implemented functionality and open to extensions. Although this phrase is often used in object-oriented programming, it applies to functional programming as well. We could consider using a higher-order function to solve the problem, again defining a log function, but this time it takes a function that prints the log internally and calls the passed function at the end:

def log(func) :
    print('run hello')
    func()

def hello() :
    print('Hello World! ')

log(hello)
Copy the code

The above code takes advantage of the fact that a function can be passed as an argument to another function, eliminating the need to modify the internal code of the original function. This is functionally implemented and does not break the internal logic of the original function, but it does break the code logic of the function caller. That is, all calls to the hello function in the original code had to be changed from hello() to log(hello), which seems more cumbersome.

Simple decorator

So, now is the time to introduce the concept of decorators, which are very good at solving these problems in Pythonic fashion. Here’s the simplest way to write a decorator:

def log(func) :
    def wrapper() :
        print('run hello')
        func()
    return wrapper

def hello() :
    print('Hello World! ')

hello = log(hello)
hello()
Copy the code

A function can be assigned to a variable, a function can be passed as an argument to another function, and a function can return a function via a return statement. Now the log function is just a decorator. We define a log function that takes a function as an argument, and inside it we define a wrapper function that, after printing the log, calls the func function passed in (hello). Return the internally defined function at the end of the log function. At the bottom of the sample code, we pass hello as an argument to the log function and assign the result to the variable Hello, which points to an internal function, Wrapper, instead of the original Hello function. Now the caller does not need to change the method of calling hello(), but it has been enhanced to automatically call hello in print(‘Hello World! ‘) before the logic to print logs. In the above code we have functionally implemented the effect of a decorator. But in fact, Python supports the decorator pattern directly at the syntactic level. All it takes is the @ symbol to make the above code more readable and maintainable.

def log(func) :
    def wrapper() :
        print('run hello')
        func()
    return wrapper

@log
def hello() :
    print('Hello World! ')

hello()
Copy the code

The @ symbol is syntactic sugar provided by Python at the syntactic level, but it is essentially the exact equivalent of hello = log(hello). This is a minimal Pythonic decorator, and no matter how complex a decorator you encounter in the future, keep in mind that it is really a function at the end of the day, but it takes advantage of some Python function features to enable it to handle more complex business scenarios.

Decorator for a function to be decorated with arguments and return values

In the actual work scenario, the functions we write are often very complex. To write a more versatile decorator, we still need to do some details. But now that you know the nature of decorators, the rest of the examples won’t be too hard to understand. You just need to use a decorator for a specific function in a specific scenario.

def log(func) :
    def wrapper(*args, **kwargs) :
        print('run hello')
        return func(*args, **kwargs)
    return wrapper

@log
def hello(name) :
    print('Hello World! ')
    return f'I am {name}. '

result = hello('xiaoming')
print(result)
Copy the code

* ARgs, **kwargs these two variable length parameters, it is a good solution to the generality of the decorator, so that when the decorator decorates any function, the parameters can be passed into the original function. The wrapper function finally calls the func function with a return statement, which returns the result of the original function to the caller.

Decorator that holds meta information about the decorator function

The wrapper function inside the log decorator prints the log code print(‘run hello’) is a fixed string if we want it to automatically change the print result based on the function name, such as print(f’run {function name}.’). Each function has a __name__ attribute that returns its function name:

def hello(name) :
    print('Hello World! ')

print(hello.__name__)  # hello
Copy the code

The problem is that with the log decorator, the original Hello function now points to the wrapper function, so if you test it, the decorated Hello function’s __name__ property has changed to the Wrapper function, which is obviously not what we want. We can solve this problem with one line of wrapper.__name__ = func.__name__, but we can do better. The built-in decorator functools.wraps in Python can help us solve this problem.

from functools import wraps

def log(func) :
    @wraps(func)
    def wrapper(*args, **kwargs) :
        print(f'run {func.__name__}')
        return func(*args, **kwargs)
    return wrapper

@log
def hello(name) :
    print('Hello World! ')
    return f'I am {name}. '

print(hello('xiaoming'))
print(hello.__name__)
Copy the code

The decorator itself takes parameters

Perhaps you want to control the logging level of the log decorator, so passing arguments to the decorator is an easy way to do this. Here’s an example of a decorator that needs to receive arguments:

from functools import wraps

def log(level) :
    def decorator(func) :
        @wraps(func)
        def wrapper(*args, **kwargs) :
            if level == 'warn':
                print(f'run {func.__name__}')
            elif level == 'info':
                pass
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log('warn')
def hello(name) :
    print('Hello World! ')
    return f'I am {name}. '

result = hello('xiaoming')
print(result)
Copy the code

The decorator with arguments has an additional layer of function nesting compared to the previous decorator. The effect is actually hello = log(‘warn’)(hello). The first call to log(‘warn’) returns the internal decorator function, This then corresponds to Hello = decorator(hello), which is essentially the same as a decorator with no arguments at this point.

Decorators are supported with or without parameters

Sometimes you may encounter more abnormal requirements, need to be able to use decorator to pass or not pass parameters, there are many solutions, I give a relatively simple and easy to understand the implementation.

from functools import wraps

def log(level) :
    if callable(level):
        @wraps(level)
        def wrapper1(*args, **kwargs) :
            print(f'run {level.__name__}')
            return level(*args, **kwargs)
        return wrapper1
    else:
        def decorator(func) :
            @wraps(func)
            def wrapper2(*args, **kwargs) :
                if level == 'warn':
                    print(f'run {func.__name__}')
                elif level == 'info':
                    pass
                return func(*args, **kwargs)
            return wrapper2
    return decorator

@log('warn')
def hello(name) :
    print('Hello World! ')
    return f'I am {name}. '

@log
def world() :
    print('world')

print(hello('xiaoming'))
world()
Copy the code

Callable can determine whether the passed argument is callable, but note that callable only supports Python3.2 and above. You can check the official documentation for details.

Class decorator

Class decorators are more flexible and powerful than function decorators. A __call__ method can be defined in a Python class so that it can itself be called without instantiation, at which point the code inside __call__ is executed.

class Log(object) :
    def __init__(self, func) :
        self._func = func

    def __call__(self) :
        print('before')
        self._func()
        print('after')

@Log
def hello() :
    print('hello world! ')

hello()
Copy the code

Decorator decoration sequence

A function can be decorated with multiple decorators at the same time. What is the order in which the decorators are decorated? So let’s explore that.

def a(func) :
    def wrapper() :
        print('a before')
        func()
        print('a after')
    return wrapper

def b(func) :
    def wrapper() :
        print('b before')
        func()
        print('b after')
    return wrapper

def c(func) :
    def wrapper() :
        print('c before')
        func()
        print('c after')
    return wrapper

@a
@b
@c
def hello() :
    print('Hello World! ')

hello()
Copy the code

The above code results:

a before
b before
c before
Hello World!
c after
b after
a after
Copy the code

The multi-decorator syntax is equivalent to Hello = a(b(c(hello)). The order of execution of this code is not hard to find from the printed results. If you’re familiar with the middleware mechanics of Node.js’s Koa2 framework, you’ll be familiar with the above code execution sequence. Python decorators actually follow the Onion model as well. The sequence of code execution for multiple decorators is like peeling an onion, starting from the outside in and then from the inside out. One question to ponder: which wrapper function does the final hello.__name__ point to inside the decorator?

Decoration actual combat

Once we understand the use of decorators, we need to use them. The use of decorators was mentioned at the beginning of this article. Let’s look at an example of using decorators in a real world scenario. Flask is a very popular micro-framework in the Python Web ecosystem, and you can check out the source code on GitHub. Here is a minimal Web application written in Flask. The @app.route(“/”) decorator binds the root route/request to the hello handler for processing. So when we start Flask Web Server, go to http://127.0.0.1:5000/ in the browser address and get the result Hello, World! .

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello() :
    return "Hello, World!"
Copy the code

Of course, more decorator usage scenarios still need to be explored by yourself.

Jianghushini.cn