2. python Deep Decorator

Keywords: Attribute Python Django

Two important concepts of decorator:

1'@'grammatical sugar
(2) Customization before and after execution without changing the original function code

The decorator is placed where a function begins to be defined, and it is worn like a hat on the head of the function. It's bound to this function. When we call this function, the first thing is not to execute it, but to pass this function as a parameter into the hat overhead, which we call the decorator, and the return value is the function object.
Use scenarios:

  • Application scenarios:
    • Flask: Routing, before_request, after_request
    • Django: csrf, caching, built-in user login authentication
    • functools: caching, warper

During an interview, the interviewer asked two questions:

1. What kind of functions have you used decorative devices to achieve?

2. How to write an ornament that can be passed on?

01. Hello, Decorator

The use of decorators is very fixed.

  1. Define a decorator (hat)

  2. Redefine your business functions or classes (people)

  3. Finally, fasten the decorator (hat) on the head of the function (person).

It's like this

def decorator(func):  # The func import calls the decorator function, retaining the original function of the function.
    def wrapper(*args, **kwargs):  # wrapper indicates that the original function is encapsulated * args, ** kw can pass in multiple parameters
        if datetime.datetime.now().year == 2020:  # New business logic
            print(datetime.datetime.now().year)
            return func(*args, **kwargs)  #Call the original function
        else:
            print('Not pass oOooO')

    return wrapper


@decorator
def test(name):
    print('hello,{}!'.format(name))


test('haha')

In fact, decorators are not necessarily coding, that is to say, you can not use decorators, it should be the emergence of our code.

  • More elegant, more clear code structure

  • Encapsulating specific functional codes into decorators to improve code reuse and readability

02. Introduction: Log Printer

First is the log printer.
Functions implemented:

  1. Before the function is executed, print a line of log to inform the host that I am going to execute the function.

  2. After the function is executed, you can't pat your butt and walk away. We are polite code, and print a line of log to inform the host, I'm done.

#This is the decorator function. The parameter func is the decorated function.
def logger(func):
    def wrapper(*args, **kw):
        print('Master, I'm ready to start executing:{} Function:'.format(func.__name__))
        # What's really going on is this line.
        func(*args, **kw)
        print('Master, I'm done.')

    return wrapper

Suppose my business function is to calculate the sum of two numbers. When you have finished writing, put a hat on it directly.

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

Then execute the add function.

add(200, 50)

Let's see what's output.

Master, I'm ready to start executing: add function:
200 + 50 = 250
 Master, I'm done.

03. Introduction: Time Timer

Let's look at the timer again.
Implementing function: As the name implies, it is to calculate the execution time of a function.

#This is a decorative function.
import time 
def timer(func):
    def wrapper(*args, **kw): 
        t1 = time.time()
        func(*args,**kw)
        t2 = time.time()
        cost_time=t2 - t1
        print("Spend time:{}second".format(cost_time))
    return wrapper

If, our function is to sleep for 10 seconds. In this way, we can better see whether the calculation time is reliable or not.

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

Let's look at the output, 10 seconds as expected.

Time spent: 10.0073800086975098 seconds

04. Advancement: Functional Decorator with Parameters

Through the above two simple introductory examples, you should be able to understand the working principle of the decorator.

However, the use of decorative appliances is far more than that. There are many articles to go into. Let's learn this knowledge thoroughly today.

Looking back at the example above, the decorator can't accept parameters. Its usage can only be applied to some simple scenarios. Decorators without parameters can only perform fixed logic on decorated functions.

Decorator itself is a function, as a function, if it can not pass parameters, then the function of this function will be very limited, can only execute fixed logic. This means that if the decorator's logic code execution needs to be adjusted according to different scenarios, if it can not be passed on, we have to write two decorators, which is obviously unreasonable.

For example, if we want to achieve a task that can send mail regularly (send a letter in a minute) and synchronize time regularly (synchronize once a day), we can implement a periodic_task decorator by ourselves. The decorator can receive parameters of a time interval and how long the interval is. Perform a task once in a while.

It can be written as follows. Because the function code is complex and not conducive to learning, it is not pasted here.

@periodic_task(spacing=60)
def send_mail():     
  pass
@periodic_task(spacing=86400)
def ntp()
  pass 

Then we create a false scene for ourselves. We can pass in a parameter in the decorator, specify nationality, and greet with our native language before the function is executed.

#Xiao Ming, Chinese
@say_hello("china")
def xiaoming(): 
  pass
#jack, American
@say_hello("america")
def jack(): 
  pass

What if we implement this ornament so that it can be realized?

It will be more complex, requiring 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 functions are actually executed
            func(*args, **kwargs)
        return deco
    return wrapper

To execute

xiaoming()
print("------------")
jack()

Look at the output.

Hello!
------------
hello.

05. High Level: Class Decorator without Parameters

These are all function-based decorators. When you read other people's code, you can often find class-based decorators.

Based on the implementation of class decorator, two built-in functions _call_ and _init_ must be implemented.
_ init_: Receives decorated functions
_ call_: Implement decorative logic.

Or take the simple example of log printing

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")

Execute and see the output

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

06. High order: parametric class ornaments

The above example without parameters, you found that no, can only print INFO level logs, under normal circumstances, we also need to print DEBUG WARNING level logs. This requires passing in parameters to the class decorator and assigning a level to the function.

Class decorators with and without parameters are quite different.

_ init_: No longer receives the decorated function, but receives the incoming parameters.
_ call_: Receives the decorated function to implement the decorated logic.

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

    def __call__(self, func): # Acceptance 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")

Let's specify the WARNING level and run it to see the output.

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

07. Implementing Decorators with Partial Functions and Classes

Most decorators are based on functions and closures, but this is not the only way to make decorators.

In fact, Python has only one requirement for whether an object can be used as a decorator (@decorator): the decorator must be a "callable" object.

For this callable object, we are most familiar with functions.

In addition to functions, classes can also be callable objects, as long as the _call_ function is implemented (the above examples have been touched on).

Another easily ignored partial function is actually a callable object.

Next, let's talk about how to use classes and partial functions to achieve a unique decorator.

As shown below, DelayFunc is a class that implements _call_. Delay returns a partial function, where delay can be used as a decorator. (The following code is taken from Python craftsmen: 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: Delays the execution of a function.
    //Meanwhile, the. eager_call method is provided for immediate execution.
    """
    # To avoid defining additional functions,
    # Direct use of functools.partial to help construct DelayFunc instances
    return functools.partial(DelayFunc, duration)

Our business function is simple, it's additive.

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

Take a look at the execution process

>>> add    # So add becomes an example of Delay
<__main__.DelayFunc object at 0x107bd0be0>
>>> 
>>> add(3,5)  # Call the instance directly, and enter _call__
Wait for 2 seconds...
8
>>> 
>>> add.func # Implementing Example Method
<function add at 0x107bef1e0>

08. How to write decorative articles?

There are three common ways to write singleton patterns in Python. One of them is to use decorative devices to achieve.

Here is my own example of the decorative version.

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

You can see that we decorate the User class with the decorative function singleton. Decorators used in classes are not very common, but as long as you are familiar with the implementation process of the decorator, it is not difficult to achieve class decoration. In the above example, the decorator is just the control of the generation of class instances.

Its instantiation process, you can refer to my debugging process here, to understand.

09. What's the use of wraps?

There is a wraps decorator in the functools standard library. You should have seen it often. What's the use of it?

Let's start with an example.

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

@wrapper
def wrapped():
    pass

print(wrapped.__name__)
#inner_function

Why does this happen? Shouldn't we go back to func?

This is not difficult to understand, because the top execution func and the bottom decorator(func) are equivalent, so the top func. _name_ is equivalent to the bottom decorator(func). _name_ of course, the name is inner_function.

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

def wrapped():
    pass

print(wrapper(wrapped).__name__)
#inner_function

How to avoid this situation? The method is to use functools. wraps decorator. Its function is to assign some attribute values of wrapped function to wrapper function, and finally make the display of attributes more in line with our intuition.

from functools import wraps

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

@wrapper
def wrapped():
    pass

print(wrapped.__name__)
# wrapped

To be precise, wraps is actually a partial function object with the following source code

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

You can see that wraps is actually calling a function update_wrapper. Knowing the principle, we can rewrite the above code and let wrapped. _name_ print out 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():
        pass

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

@wrapper
def wrapped():
    pass

print(wrapped.__name__)

10. Built-in Decorator: property

Above all, we introduce custom decorators.

In fact, Python language itself has some ornaments. For example, property is a built-in decorator that we are all familiar with.

It usually exists in classes, and a function can be defined as an attribute whose value is the content of the function return.

Usually we bind properties to instances like this

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 attribute
xiaoming.age

# Delete attribute
del xiaoming.age

But a little experienced developers, it can be seen at a glance that this directly exposes the attributes, although it is simple to write, but the value of attributes can not be legitimate restrictions. In order to achieve this function, we can write as follows.

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 is not valid: age must be a value!')
        if not 0 < age < 100:
            raise ValueError('Illegal input: 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 attribute
xiaoming.get_age()

# Delete attribute
xiaoming.del_age()

Although the above code design can define variables, it can be found that both acquisition and assignment (through functions) are different from what we usually see.
According to our thinking habits, this should be the case.

# assignment
xiaoming.age = 25

# Obtain
xiaoming.age

So how can we achieve this? See 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 is not valid: age must be a value!')
        if not 0 < value < 100:
            raise ValueError('Illegal input: age range must be 0-100')
        self._age=value

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

xiaoming = Student("Xiao Ming")

# set a property
xiaoming.age = 25

# Query attribute
xiaoming.age

# Delete attribute
del xiaoming.age

A function decorated with @property defines a function as an attribute whose value is the content of the function return. At the same time, this function will be transformed into another decorator. Like the @age.setter and @age.deleter we used later.

@ age.setter allows us to assign values directly using XiaoMing.age = 25.
@ age.deleter allows us to delete attributes in a way like del XiaoMing.age.

Posted by Nandini on Mon, 16 Sep 2019 01:45:01 -0700