Decorators are a well-known design pattern that is often used in scenarios with faceted requirements, such as insert logging, performance testing, and transaction processing. Decorators are a great way to solve these problems. With decorators, we can isolate and reuse the same code in a large number of functions that have nothing to do with the function itself. In a nutshell, the purpose of decorators is to add extra functionality to an existing object.

1. Where does demand come from

The definition of a decorator is quite abstract, so let’s look at a small example.

def foo():
    print('in foo()')
foo()
Copy the code

It’s a boring function, right. But suddenly someone even more boring, let’s call him Mr. B, says I want to see how long it takes to execute this function, okay, so we can do this:

import time

def foo():
    start = time.time()
    print('in foo()')
    time.sleep(2)
    end = time.time()
    print(f'used:{end - start}')

foo()
Copy the code

Good. The functionality looks impeccable. But B suddenly stopped looking at this function and became more interested in another function called foo2.

What to do? If you copy the above code to foo2, this is a big mistake. And what if B goes on to look at other functions?

2. Constant should change, change is also

Remember, functions are first-class citizens in Python, so we could consider redefining a function timeit, passing it a reference to Foo, then calling foo in timeit and timing it, thus achieving our goal of not changing foo’s definition, and no matter how many functions B looks at, We don’t even have to change the function definition!

import time

def foo():
    print('in foo()')

def timeit(func):
    start = time.time()
    func()
    time.sleep(2)
    end = time.time()
    print('used:', end - start)

timeit(foo)

Copy the code

There seems to be no logical problem, everything is fine and working fine! … Wait, it looks like we changed the calling part of the code. Instead of calling foo(), we’ll call timeit(foo). That way, if foo is called at all N points, you have to change the code at all N points. Or, more extreme, consider that the code that is called somewhere in the code can’t change the situation, such as if the function is something you gave someone else to use.

3. Change as little as possible

In this case, let’s figure out a way not to modify the calling code; Without modifying the calling code, this means that calling foo() needs to have the effect of calling timeit(foo). We could think of assigning timeit to foo, but timeit seems to take an argument… Think of ways to unify the parameters! If timeit(foo) does not call directly, but returns a function consistent with foo’s argument list… We assign the return value of timeit(foo) to foo, and the code that calls foo() doesn’t change at all!

# -* -coding: utF-8 -* -import time def foo(): print('in foo()') # def timeit(func): Def wrapper() = def wrapper(); start = time.time() func() time.sleep(2) end = time.time() print('used:', End - start) # return return wrapper foo = timeit(foo) foo()Copy the code

So, a simple timer is ready! We can do this by simply adding foo = timeit(foo) before calling foo after defining foo, which is the concept of a decorator that looks like foo is decorated by timeit. In this example, the timing of function entry and exit is called an Aspect. This approach to Programming is called aspect-oriented Programming. In contrast to the top-down approach of traditional programming, it is like inserting a piece of logic horizontally into the flow of function execution. Reduce the amount of repetitive code in a particular business area. There are quite a few other terms for aspect programming, but I won’t go into them here. If you’re interested, you can look them up.

This example is for demonstration purposes only, and does not consider foo to have parameters and return values, so it’s up to you to make it better 🙂

As if this code could not be more compact, Python provides a syntactic sugar to reduce the number of characters to be entered.

import time


def timeit(func):
    def wrapper():
        start = time.time()
        func()
        time.sleep(2)
        end = time.time()
        print('used:', end - start)

    return wrapper


@timeit
def foo():
    print('in foo()')


foo()
Copy the code

Focus on @timeit on line 11. Adding that line to the definition is exactly the same as writing foo = timeit(foo), and don’t think there’s any magic in @. As well as having less character input, there is an added benefit: it gives it a more decorator feel.

As you can see, a decorator in Python is essentially a function that takes another function as an argument and replaces it with a new modified function.

4. Use decorators for functions that take arguments

If the function you want to wrap has arguments, you don’t need to bother. As long as the parameters and return values of the wrapped function are the same as the original function, the wrapped function returns the wrapped function object

import datetime,time def out(func): def inner(*args): start = datetime.datetime.now() func(*args) end = datetime.datetime.now() print(end-start) print("out and inner") return  inner @out def myfunc(*args): time.sleep(1) print("args is{}".format(args)) myfunc("lalalal")Copy the code

5. Give the decorator parameters

Passing arguments to the decorator is not difficult either, just an extra layer of wrapping compared to the previous example

#coding:utf-8 def outermost(*args): def out(func): print (" {}". Format (args)) def inner(*args): print("innet start") func(*args) print ("inner end") return inner return out @outermost(666) def myfun(*args): Format (args)) myFun (" Zhangkun ")Copy the code

6. Decorators with class parameters

It doesn’t matter what type the argument is, it doesn’t matter if it’s a class, right

class locker:
    def __init__(self):
        print("locker.__init__() should be not called")

    @staticmethod
    def acquire():
        print("locker.acquire() static method be called")

    @staticmethod
    def release():
        print("locker.release() static method be called")

def outermost(cls):
    def out(func):
        def inner():
            cls.acquire()
            func()
            cls.release()
        return inner
    return out

@outermost(locker)
def myfunc():
    print("myfunc called")

myfunc()
Copy the code

7. Apply multiple decorators to a function

A function can have more than one decorator, but pay attention to the order

class mylocker: def __init__(self): print("mylocker.__init__() called.") @staticmethod def acquire(): print("mylocker.acquire() called.") @staticmethod def unlock(): print(" mylocker.unlock() called.") class lockerex(mylocker): @staticmethod def acquire(): print("lockerex.acquire() called.") @staticmethod def unlock(): print(" lockerex.unlock() called.") def lockhelper(cls): def _deco(func): def __deco2(*args, **kwargs): print("before %s called." % func.__name__) cls.acquire() try: return func(*args, **kwargs) finally: cls.unlock() return __deco2 return _deco class example: @lockhelper(mylocker) @lockhelper(lockerex) def myfunc2(self, a, b): Print (" myfunc2() called.") print(a+b) a = example() a.myfunc2(1,2)Copy the code

8. As a class

While decorators can almost always be implemented as functions, in some cases it may be better to use user-defined classes

import time class DerocatorAsClass: def __init__(self,funcation): self.funcation = funcation def __call__(self, *args, **kwargs): Result = self.funcation(*args,**kwargs) print('3333333333') # return result @DerocatorAsClass def foo(): print('in foo()') foo()Copy the code

As in the example above, it is also convenient to use classes as decorators