The contents of this article are as follows:

  1. Decorator grammar sugar
  2. Getting started: Log printer
  3. Beginner usage: time timer
  4. Advanced usage: function decorator with arguments
  5. Advanced usage: class decorator with no arguments
  6. Advanced usage: class decorator with arguments
  7. Use partial functions and classes to implement decorators
  8. How to write a decorator that decorates a class?
  9. What’s the use of wraps?
  10. Built-in decorator: Property
  11. Other decorator: Decorator actual combat

01. Decorator grammar sugar

If you’ve been around Python for a while, you’re probably familiar with the @ symbol, which is the syntactic sugar of decorators.

It sits at the beginning of a function definition, and it sits like a hat on the head of the function. It’s bound to this function. When we call this function, the first thing we do is not execute the function, but pass the function as an argument to the hat on top of it, which we call the decorator function or decorator.

What can a decorator do, you ask? All I can say is that decorators are as powerful as your imagination.

The way decorators are used is very fixed:

  • Define a decorator function (hat) (you can also use classes, partial functions)
  • Redefine your business functions, or classes (people)
  • And finally put the hat on the man’s head

There are many simple uses for decorators, but here are two common ones.

  • Log printer
  • Time timer

02. Getting started usage: Log printer

The first is the log printer. What it does is

  • Before the function is executed, print a line of log to inform the host that I am going to execute the function.
  • After the function is executed, you can’t just leave. I’m polite code. Print a line of log to tell the host that I’m done.
This is the decorator function
def logger(func):
    def wrapper(*args, **kw):
        print('I'm ready to compute the: {} function :'.format(func.__name__))

        # this line is actually executed.
        func(*args, **kw)

        print('Aha, I'm done. Get yourself a chicken leg!! ')
    return wrapper
Copy the code

Let’s say my business function is to add up two numbers. When you’re done, put your hat on it.

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))
Copy the code

And then let’s calculate it.

add(200.50)
Copy the code

Let’s see what comes out. Isn’t it amazing?

I'm ready to start calculating the: add function:200 + 50 = 250Aha, I'm done. Get yourself a chicken leg!Copy the code

03. Getting started: Time timer

Consider the time timer implementation: as the name suggests, it counts the execution time of a function.

This is the decorator function
def timer(func):
    def wrapper(*args, **kw):
        t1=time.time()
        # This is where the function is actually executed
        func(*args, **kw)
        t2=time.time()

        # Count the time
        cost_time = t2-t1 
        print("Elapsed time: {} seconds".format(cost_time))
    return wrapper
Copy the code

Let’s say our function is to sleep for 10 seconds. It’s also a good way to see if this calculation is reliable.

import time

@timer
def want_sleep(sleep_time):
    time.sleep(sleep_time)

want_sleep(10)
Copy the code

Let’s see, output. It’s really 10 seconds.

Time: 10.0073800086975098 secondsCopy the code

04. Advanced usage: Function decorator with arguments

Through the above simple introduction, you may already feel the magic of decoration.

But decorators are more than that. We’re going to go through this today.

In the example above, the decorator cannot accept arguments. It can only be used in some simple scenarios. Decorators that do not take arguments can only execute fixed logic on the decorator function.

If you’re experienced, you’ve often seen decorators with parameters in projects.

The decorator itself is a function, and since it can’t carry a function as a function, its functionality is limited. Only fixed logic can be executed. This is undoubtedly very unreasonable. And if we’re going to use two logics that are basically the same thing, just different in some places. Otherwise, we have to write two decorators. Xiao Ming felt this was intolerable.

So how does the decorator implement parameter passing? It’s a bit more complicated and requires two layers of nesting.

Again, let’s take an example.

We are going to say a greeting at the execution of these two functions, depending on their nationality.

def american(a):
    print("I am from America.")

def chinese(a):
    print("I'm from China.")
Copy the code

In the two of them put on the decoration, it is necessary to say to the decoration, which country is this person, and then the decoration will make a judgment, hit the corresponding hello.

With the hat on, it looks like this.

@say_hello("china")
def chinese(a):
    print("I'm from China.")

@say_hello("america")
def american(a):
    print("I am from America.")
Copy the code

All we need is a hat. To define this, we need two layers of nesting.

def say_hello(contry):
    def wrapper(func):
        def deco(*args, **kwargs):
            if contry == "china":
                print("Hello!")
            elif contry == "america":
                print('hello.')
            else:
                return

            # where the function is actually executed
            func(*args, **kwargs)
        return deco
    return wrapper

Copy the code

To perform a

american()
print("-- -- -- -- -- -- -- -- -- -- -- --")
chinese()

Copy the code

Look at the output.

Hello! I come from China. ------------ hello. I amfrom America

Copy the code

Emmmm, this is NB…

05. Advanced usage: Class decorator with no arguments

All of these are function-based decorators, and it’s not uncommon to find class-based decorators when reading other people’s code.

Class decorator-based implementations must implement __call__ and __init__ built-in functions. __init__ : receives the decorated function __call__ : implements the decorated logic.

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

    def __call__(self, *args, **kwargs):
        print("[INFO]: the function {func}() is running..."\
            .format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logger
def say(something):
    print("say {}!".format(something))

say("hello")

Copy the code

Let’s do it and see the output

[INFO]: the function say() is running...
say hello!

Copy the code

06. Advanced usage: Class decorator with arguments

You can only print INFO logs. In normal cases, you also need to print DEBUG WARNING logs. This requires passing in arguments to the class decorator to specify the level of the function.

There is a big difference between class decorators with and without arguments.

__init__ : Instead of receiving decorated functions, it receives passed arguments. __call__ : Receives decorator functions and implements decorator logic.

class logger(object):
    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func): # accept function
        def wrapper(*args, **kwargs):
            print("[{level}]: the function {func}() is running..."\
                .format(level=self.level, func=func.__name__))
            func(*args, **kwargs)
        return wrapper  # return function

@logger(level='WARNING')
def say(something):
    print("say {}!".format(something))

say("hello")

Copy the code

Let’s specify the WARNING level, run it, and see the output.

[WARNING]: the function say() is running...
say hello!

Copy the code

07. Use partial functions and classes to implement decorators

Most decorators are implemented based on functions and closures, but that’s not the only way to make decorators.

In fact, Python has only one requirement for an object to be used in the @decorator form: The decorator must be a callable object.

The callable object we are most familiar with is the function.

In addition to functions, classes can also be callable objects, as long as they implement __call__ functions (which the boxes above touched on), and less commonly used partial functions are callable objects.

Let’s talk about how to use a combination of classes and partial functions to create a different decorator.

As shown below, DelayFunc is a class that implements __call__, and delay returns a partial function, where Delay can be used as a decorator. (The following code is from Python Craftsman: Tips for using decorators)

import time
import functools

class DelayFunc:
    def __init__(self, duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration}seconds... ')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    Decorator: postponing the execution of a function. Also provides.eager_call method to execute immediately """
    To avoid defining additional functions,
	Use functools.partial directly to help construct DelayFunc instances
    return functools.partial(DelayFunc, duration)

Copy the code

Our business function is simple: add

@delay(duration=2)
def add(a, b):
    return a+b

Copy the code

Take a look at the implementation

>>> add    Add becomes an instance of Delay
<__main__.DelayFunc object at 0x107bd0be0>
>>> 
>>> add(3.5)  Call the instance directly and go to __call__
Wait for 2 seconds...
8
>>> 
>>> add.func Implement instance methods
<function add at 0x107bef1e0>

Copy the code

08. How to write a decorator that can decorate a class?

There are three common ways to write a singleton pattern in Python. One of these is done with decorators.

Here is a singleton of the decorator version that I wrote myself.

instances = {}

def singleton(cls):
	def get_instance(*args, **kw):
		cls_name = cls.__name__
		print('1 = = = = = = = = =')
		if not cls_name in instances:
			print('2 = = = = = = = = =')
			instance = cls(*args, **kw)
			instances[cls_name] = instance
		return instances[cls_name]
	return get_instance

@singleton
class User:
	_instance = None

	def __init__(self, name):
		print(3 '= = = = = = = = =')
		self.name = name

Copy the code

You can see that we decorated the User class with the singleton decorator function. Decorators are not very common for classes, but it is not difficult to decorate classes if you are familiar with the implementation process of decorators. In the example above, the decorator simply controls the generation of class instances.

In fact, the process of instantiation, you can refer to my debugging process here, to understand.

What’s the use of wraps?

There’s a wraps in the FuncTools library that you’ve probably seen a lot, so what’s the use?

Let’s start with an example

def wrapper(func):
    def inner_function(a):
        pass
    return inner_function

@wrapper
def wrapped(a):
    pass

print(wrapped.__name__)
#inner_function

Copy the code

Why is that? Shouldn’t you return func?

This also makes sense because the top execution of func is equivalent to the bottom decorator(func), so the top func.__name__ is equivalent to the bottom decorator(func).__name__, which of course is inner_function

def wrapper(func):
    def inner_function(a):
        pass
    return inner_function

def wrapped(a):
    pass

print(wrapper(wrapped).__name__)
#inner_function

Copy the code

So how do you avoid that? The method is to use the functools. wraps decorator, which assigns some attributes of the wrapped function to the Wrapper function to make the attributes appear more intuitive.

from functools import wraps

def wrapper(func):
    @wraps(func)
    def inner_function(a):
        pass
    return inner_function

@wrapper
def wrapped(a):
    pass

print(wrapped.__name__)
# wrapped
Copy the code

To be precise, wraps are actually partial

def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

Copy the code

Wrapped.__name__ can also print wrapped without wraps. The code is as follows:

from functools import update_wrapper

WRAPPER_ASSIGNMENTS = ('__module__'.'__name__'.'__qualname__'.'__doc__'.'__annotations__')

def wrapper(func):
    def inner_function(a):
        pass

    update_wrapper(inner_function, func, assigned=WRAPPER_ASSIGNMENTS)
    return inner_function

@wrapper
def wrapped(a):
    pass

print(wrapped.__name__)
Copy the code

10. Built-in decorator: Property

Above, we introduce custom decorators.

The Python language itself has some decorators. The built-in decorator property, for example, is familiar.

It usually exists in a class and allows you to define a function as a property whose value is the content of the function return.

This is how we normally bind properties to instances

class Student(object):
    def __init__(self, name, age=None):
        self.name = name
        self.age = age

# instantiation
XiaoMing = Student("Xiao Ming")

# add attribute
XiaoMing.age=25

# query attributes
XiaoMing.age

# delete attribute
del XiaoMing.age

Copy the code

But as experienced developers can quickly see, exposing attributes directly in this way, while simple to write, does not impose legal restrictions on their values. To do this, we can write it like this.

class Student(object):
    def __init__(self, name):
        self.name = name
        self.name = None

    def set_age(self, age):
        if not isinstance(age, int):
            raise ValueError('Input invalid: Age must be numeric! ')
        if not 0 < age < 100:
            raise ValueError('Input illegal: age range must be 0-100')
        self._age=age

    def get_age(self):
        return self._age

    def del_age(self):
        self._age = None


XiaoMing = Student("Xiao Ming")

# add attribute
XiaoMing.set_age(25)

# query attributes
XiaoMing.get_age()

# delete attribute
XiaoMing.del_age()

Copy the code

While the above code design can define variables, it can be found that both fetching and assigning (via functions) are different from what we are used to seeing. That’s how we think.

# assignment
XiaoMing.age = 25

# get
XiaoMing.age

Copy the code

So how do we achieve this way. Look at the code below.

class Student(object):
    def __init__(self, name):
        self.name = name
        self.name = None

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise ValueError('Input invalid: Age must be numeric! ')
        if not 0 < value < 100:
            raise ValueError('Input illegal: age range must be 0-100')
        self._age=value

    @age.deleter
    def age(self):
        del self._age

XiaoMing = Student("Xiao Ming")

# set attributes
XiaoMing.age = 25

# query attributes
XiaoMing.age

# delete attribute
del XiaoMing.age

Copy the code

A function decorated with @property defines a function as a property whose value is the content of the function return. At the same time, it turns this function into another decorator. Just like we used @age.setter and @age.deleter later.

The @age.setter allows us to assign directly using XiaoMing. Age = 25. @age.deleter allows us to delete properties using del XiaoMing. Age.

The underlying implementation mechanism for property is “descriptors”, which I’ve written about.

Here also introduce it, just string together these seemingly scattered articles.

Below, I write a class that uses property to make Math an attribute of the class instance

class Student:
    def __init__(self, name):
        self.name = name

    @property
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

Copy the code

Why is property underlying the descriptor protocol? Click on the source code of property through PyCharm. Unfortunately, it is just a pseudo-source code like a document without its specific implementation logic.

However, from this pseudo-source magic function structure composition, you can generally know its implementation logic.

Here, I realized the class property feature myself by imitating its function structure and combining “descriptor protocol”.

The code is as follows:

class TestProperty(object):

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        print("in __get__")
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError
        return self.fget(obj)

    def __set__(self, obj, value):
        print("in __set__")
        if self.fset is None:
            raise AttributeError
        self.fset(obj, value)

    def __delete__(self, obj):
        print("in __delete__")
        if self.fdel is None:
            raise AttributeError
        self.fdel(obj)


    def getter(self, fget):
        print("in getter")
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        print("in setter")
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        print("in deleter")
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Copy the code

And then the Student class, we’ll change it to the following

class Student:
    def __init__(self, name):
        self.name = name

    # This is the only place that has changed
    @TestProperty
    def math(self):
        return self._math

    @math.setter
    def math(self, value):
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

Copy the code

To minimize your confusion, here are two things:

  1. useTestPropertyAfter the decoration,mathIt’s not a function anymore, it’s a functionTestPropertyClass. So the second Math function can be usedmath.setterTo decorate, essentially callTestProperty.setterTo create a new oneTestPropertyInstance assigned to the secondmath.
  2. The first onemathAnd the secondmathIt’s two different thingsTestPropertyInstance. But they both belong to the same descriptor class (TestProperty), which is entered when math is assigned to mathTestProperty.__set__When math is evaluated, it entersTestProperty.__get__. On closer inspection, the final access is to the Student instance_mathProperties.

With all that said, let’s just run it a little bit more intuitively.

This line is printed directly after the TestProperty is instantiated and assigned to the second Math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90

Copy the code

If you have any questions about how the above code works, please be sure to understand the above two points, which are quite critical.

11. Other decorators: Actual decorators

After reading and understanding the above, you are a Master of Python. Don’t doubt it, be confident, because many people don’t realize there are so many uses for decorators.

Look in xiaoming, use decorator, can achieve the following purpose:

  • Make the code more readable, forcing higher grid;
  • The code structure is clearer and the code redundancy is lower.

Just xiao Ming has a scene recently, which can be well realized with decorators.

This is a decorator that implements a timeout control function. If a timeout occurs, a timeout exception is thrown.

If you’re interested, take a look.

import signal

class TimeoutException(Exception):
    def __init__(self, error='Timeout waiting for response from Cloud'):
        Exception.__init__(self, error)


def timeout_limit(timeout_time):
    def wraps(func):
        def handler(signum, frame):
            raise TimeoutException()

        def deco(*args, **kwargs):
            signal.signal(signal.SIGALRM, handler)
            signal.alarm(timeout_time)
            func(*args, **kwargs)
            signal.alarm(0)
        return deco
    return wraps

Copy the code