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
- Define a decorator (hat)
- Redefine your business functions or classes (people)
- 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.
- 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.
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:
- 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 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.