Photo: Unsplash.com by Simon Couball

After reading this article still do not understand decorators, only one possibility, that I write is not clear enough, encourage me.

Before I get to Python decorators, I want to give an example that’s a little dirty, but very relevant to the topic of decorators.

Everybody has underwear main function is to use hide shame, but arrived winter it cannot be us windbreak keep out cold, do how? One of the things we came up with was to make the underwear a little bit thicker and longer, so that it would not only provide shame, but it would also provide warmth, but the problem was that when we made the underwear into pants, it would still provide shame, but it wasn’t really a pair of underwear anymore. So smart people invented pants, under the premise of not affecting underwear, directly put the pants in the underwear outside, so underwear or underwear, with pants after the baby is no longer cold. Decorators are like trousers, which provide warmth to our body without affecting the function of underwear.

Python functions can be passed to another function as arguments, just like normal variables. For example:

def foo():
    print("foo")
 def bar(func):
    func()
 bar(foo)Copy the code

Let’s get back to the subject. A decorator is essentially a Python function or class that allows other functions or classes to add additional functionality without making any code changes, and the return value of the decorator is also a function/class object. It is often used in scenarios with faceted requirements, such as logging insertion, performance testing, transaction processing, caching, permission verification, etc. Decorators are an excellent design for solving these problems. With decorators, we can extract a lot of code that is similar to the function itself and continue to reuse it. In a nutshell, the purpose of decorators is to add extra functionality to an existing object.

Let’s start with a simple example, though the actual code can be much more complex:

def foo():
    print('i am foo')Copy the code

Now there is a new requirement to log the execution of a function, so add the logging code to the code:

def foo():
    print('i am foo')
    logging.info("foo is running")Copy the code

What if functions bar() and bar2() have similar requirements? Write another logging in bar function? This results in a lot of identical code. To reduce code duplication, we can define a new function that handles the log, and then execute the real business code

def use_logging(func):
    logging.warn("%s is running" % func.__name__)
    func()
 def foo():
    print('i am foo')
 use_logging(foo)Copy the code

This is logically ok, and the functionality is implemented, but instead of calling the actual business logic foo function, we call use_logging, which breaks the original code structure. Now that we have to pass the original foo function to use_logging every time, is there a better way? Of course, the answer is decorators.

Simple decorator

def use_logging(func): def wrapper(): Logging.warn ("%s is running" % func.__name__) return func() # Foo () return wrapper def foo(): Print (' I am foo') foo = use_logging(foo) # because decorator use_logging(foo) returns time function object wrapper, This statement is equivalent to foo = wrapper foo() # executing foo() equals executing wrapper()Copy the code

Use_logging is a decorator. It’s a normal function that wraps func, the function that performs the real business logic. It looks like foo is decorated with use_logging, and use_logging returns a function, The name of this function is Wrapper. In this case, the function entry and exit is called a cross section, and this type of programming is called section-oriented programming.

@ syntactic sugar

If you’ve been around Python for a while, you’re probably familiar with the @ symbol, which is the syntactic sugar of the decorator, placed at the beginning of the function definition so that you don’t have to reassign at the end.

def use_logging(func):
    def wrapper():
        logging.warn("%s is running" % func.__name__)
        return func()
    return wrapper
 @use_logging
def foo():
    print("i am foo")
 foo()Copy the code

As shown above, with @, we can omit foo = use_logging(foo) and call foo() to get the desired result. You see, foo() doesn’t need to be modified at all. It just needs to be decorated where it is defined, and it will be called just as before. If we have other similar functions, we can continue to call decorators to decorate the function without having to change the function repeatedly or add new wrapper. In this way, we improve our program’s reusability and readability.

Decorators are so easy to use in Python because Python functions can be passed as arguments to other functions, can be assigned to other variables, can be returned as values, and can be defined in another function just like ordinary objects.

* args, * * kwargs

One might ask, what if my business logic function foo needs arguments? Such as:

def foo(name):
    print("i am %s" % name)Copy the code

We can specify arguments to the Wrapper function:

def wrapper(name):
        logging.warn("%s is running" % func.__name__)
        return func(name)
    return wrapperCopy the code

This allows parameters defined in foo to be defined in the Wrapper function. At this point, someone asks, what if foo takes two arguments? Three parameters? What’s more, I might post a lot of them. When the decorator doesn’t know exactly how many arguments foo has, we can use *args instead:

def wrapper(*args):
        logging.warn("%s is running" % func.__name__)
        return func(*args)
    return wrapperCopy the code

That way, no matter how many parameters Foo defines, I can pass them to func completely. This does not affect Foo’s business logic. What if foo also defines some keyword arguments? Such as:

def foo(name, age=None, height=None):
    print("I am %s, age %s, height %s" % (name, age, height))Copy the code

In this case, you can specify the wrapper function as the keyword function:

def wrapper(*args, **kwargs): Warn ("%s is running" % func.__name__) return func(*args, **kwargs) return wrapperCopy the code

Decorator with parameters

Decorators also have greater flexibility, such as the parameterized decorator, which in the above decorator call accepts only the function foo that performs the business. The syntax of the decorator allows us to provide additional arguments, such as @decorator(a), when called. This provides greater flexibility in the writing and use of decorators. For example, we can specify the level of logging in the decorator, because different business functions may require different levels of logging.

def use_logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if level == "warn":
                logging.warn("%s is running" % func.__name__)
            elif level == "info":
                logging.info("%s is running" % func.__name__)
            return func(*args)
        return wrapper
    return decorator
 @use_logging(level="warn")
def foo(name='foo'):
    print("i am %s" % name)
 foo()Copy the code

Use_logging above is a decorator that allows arguments. It is actually a function wrapper around the original decorator and returns a decorator. We can think of it as a closure with parameters. When we call @use_logging(level=”warn”), Python can detect this layer of encapsulation and pass parameters to the decorator’s environment.

@use_logging(level=”warn”) is equivalent to @decorator

Class decorator

That’s right, decorators can be classes as well as functions. Compared with function decorators, class decorators have the advantages of greater flexibility, higher cohesion, and encapsulation. Using class decorators relies heavily on the class’s __call__ method, which is called when a decorator is attached to a function using the @ form.

class Foo(object):
    def __init__(self, func):
        self._func = func
    def __call__(self):
        print ('class decorator runing')
        self._func()
        print ('class decorator ending')
 @Foo
def bar():
    print ('bar')
 bar()Copy the code

functools.wraps

The use of decorators greatly reuses the code, but it has a disadvantage that the meta-information of the original function is missing, such as the function docString, __name__, argument list.

Def logged(func): def with_logging(*args, **kwargs) Print func.__name__ # print func.__doc__ # print None return func(*args, **kwargs) return with_logging # function @logged def f(x): """does some math""" return x + x * x logged(f)Copy the code

F has been replaced by with_logging, and of course its docstring, __name__, becomes information for the with_logging function. Fortunately, we have functools.wraps, which are also decorators, which copy the meta information of the original function into the func function inside the decorator, so that the func function inside the decorator has the same meta information as foo.

from functools import wraps def logged(func): @wraps(func) def with_logging(*args, **kwargs): Print func.__name__ # print func.__doc__ # print 'does some math' return func(*args, **kwargs) return with_logging @logged def f(x): """does some math""" return x + x * xCopy the code

Decorator sequence

A function can also define multiple decorators at the same time, such as:

@a
@b
@c
def f ():
    passCopy the code

It is executed from the inside out, first calling the innermost decorator, last calling the outermost decorator, which is equivalent to

f = a(b(c(f)))Copy the code

Share Python dry and warm content by clicking on the public account “A programmer’s micro site”