The contents of this article are as follows:
- Decorator grammar sugar
- Getting started: Log printer
- Beginner usage: time timer
- Advanced usage: function decorator with arguments
- Advanced usage: class decorator with no arguments
- Advanced usage: class decorator with arguments
- Use partial functions and classes to implement decorators
- How to write a decorator that decorates a class?
- What’s the use of wraps?
- Built-in decorator: Property
- 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:
- use
TestProperty
After the decoration,math
It’s not a function anymore, it’s a functionTestProperty
Class. So the second Math function can be usedmath.setter
To decorate, essentially callTestProperty.setter
To create a new oneTestProperty
Instance assigned to the secondmath
. - The first one
math
And the secondmath
It’s two different thingsTestProperty
Instance. 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_math
Properties.
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