As you know, the @ symbol is the syntactic sugar of decorators. The function behind the @ symbol is the main character of this article: decorators.

The decorator is placed at the beginning of a function definition and is worn like a hat over the function’s head. 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 a decorator.

When I first started my career as a programmer, I was asked two questions:

1. What functions have you used decorators to achieve?

2, how to write a decorator that can pass parameters?

With very limited practical experience at that time, I could only answer some very simple usages of the first question, but failed to answer the second question.

With these two questions in mind, I began to systematically learn all the contents of the decorator. These have been sorted out in their own blog, today it has a lot of supplements and corrections, published here to share with you. Hope to just enter and advanced friends can provide some reference.

01. Hello, decorator

The way decorators are used is very fixed

  1. Define a decorator (hat)
  2. Redefine your business functions or classes (people)
  3. Finally, attach the decorator (hat) to the function (person)

It looks something like this

def decorator(func):
    def wrapper(*args, **kw):
        return func()
    return wrapper

@decorator
def function() :print("hello, decorator")
Copy the code

In fact, decorators are not a coding necessity, which means that you don’t have to use decorators at all, and it should be there to make our code

  • More elegant, clearer code structure
  • Encapsulate the code to achieve specific functions into decorators to improve code reuse rate and code readability

Next, I’ll show you how to write a variety of simple and complex decorators with examples.

02. Getting started: Log printer

The first is the log printer.

  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 just leave. I’m polite code. Print a line of log to tell the host that I’m done.
Def logger(func): def wrapper(*args, **kw): print()'Master, I'm ready to execute the: {} function :'.format(func.__name__)) # This line is actually executed. func(*args, **kw) print('Master, I'm done. ')
    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

Then execute the add function.

add(200.50)
Copy the code

So let’s see what’s the output?

Master, I'm ready to execute the: add function:200 + 50 = 250Master, I'm done.Copy the code

03. Getting started: Time timers

Consider the time timer

Def timer(func): def wrapper(*args, **kw): Func (*args, **kw) t2=time.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 look at the output, 10 seconds as expected.

Take time:10.0073800086975098secondsCopy the code

04. Advanced: Function decorator with parameters

With the two simple introductory examples above, you should get a sense of how a decorator works.

However, the use of decorative ware is much more than that, further research, there is a great article. Let’s talk about it today.

Going back to the example above, decorators 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.

The decorator itself is a function, and as a function, if it can’t pass arguments, the function is limited and can only execute fixed logic. This means that if the execution of the decorator logic needs to be adjusted for different scenarios, we have to write two decorators if the parameters cannot be passed, which is obviously not reasonable.

Such as we want to achieve a mission to regular email (one minute to send an), timing for time synchronization task (synchronous once a day), can you achieve a periodic_task decorator (time), the decorators can receive a time interval parameters, how long interval to perform a task.

You can write it like this, but I won’t post it here because the code for this function is too complicated to learn.

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

@periodic_task(spacing=86400)
def ntp()
    pass 
Copy the code

Let’s create a fake scene of our own. We can pass a parameter in the decorator to specify the nationality and say hello in the native language of our country before the function is executed.

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

What if we implement this decorator so that it can pass parameters?

It can be complicated and requires 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

Let’s do that

xiaoming()
print("-- -- -- -- -- -- -- -- -- -- -- --")
jack()
Copy the code

Look at the output.

Hello! ------------ hello.Copy the code

05. Advanced: 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.

Again, 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")
Copy the code

Let’s do it and see the output

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

06. Advanced: Class decorator with parameters

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): # def wrapper(*args, **kwargs):"[{level}]: the function {func}() is running..."
                .format(level=self.level, func=func.__name__))
            func(*args, **kwargs)
        returnWrapper # returns the 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, a class can also be a Callable object, as long as the __call__ function is implemented (as the previous examples have touched on).

Another easily overlooked partial function is actually a Callable object.

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 for immediate execution"""To avoid defining additional functions here, use the functools.partial help directly to construct DelayFunc instancesreturn 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 # see that add becomes an instance of Delay. __main__.DelayFunc object at0x107bd0be0> > > > > > > add(3.5__call__ Waitfor 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():
        pass
    return inner_function

@wrapper
def wrapped():
    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():
        pass
    return inner_function

def wrapped():
    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():
        pass
    return inner_function

@wrapper
def wrapped():
    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():
        pass

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

@wrapper
def wrapped():
    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"Xiao Ming") # add attribute xiaoming. Age =25# delete del xiaoming. AgeCopy 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 ()25Del_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.

# assign xiaoming. Age =25# for xiaoming. AgeCopy 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 the property xiaoming. Age =25# delete del xiaoming. AgeCopy 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 @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 Mathinsetter > > > > > > s1.math =90
in__set__ > > > s1.mathin __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.

In my opinion, using decorators can achieve the following goals:

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

I happen to have a scene in the recent, can use the decorator to achieve a good, temporarily put up to have a look.

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

That’s all I have to say about decorators.

Click to become a Registered member of the Community.