When we use a flash like framework, we must have come across such code:
@app.route('/') def index(): return '<h1>Hello World!</h1>'
Here's @ app Route () is the syntax sugar of the modifier. As for the modifier of Python, I believe many people know its existence and usage like me, but they don't have a deep understanding.
This article is a summary of several tutorials and technical books to understand Python modifiers from shallow to deep. This paper will study, analyze and practice the connotation of Python modifier from the following points:
Fundamentals:
-
The principle of decorator -- calling function in function
-
@Use of grammar sugar
-
Use decorator with parameters and custom parameters
-
Use @ functools Wrap() preserves the meta information of the original function
-
Class decorator
-
Nesting of multiple decorators
Advanced:
-
How does a Python program calculate the syntax of a decorator
-
On closure function from decorator
application:
-
Authentication
-
journal
-
cache
Through the above contents, you should be able to have a deeper understanding of Python decorators. This article draws lessons from some excellent tutorials and books. There will be a link to the corresponding reference materials at the end of the article, and there will also be a description of the corresponding code in the article. This article is described in simple and easy to understand language as much as possible, but in view of my limited level, there may be unclear explanations or omissions. Please communicate and correct. I hope this article can help you.
Basic chapter
The essence of ornament
Let's start with a code:
def sayhi_to(name): print('Hi, %s' % name) say = sayhi_to say('speculatecat') >> Hi, speculatecat
We first define a sayhi_to, and then sayhi_to is assigned to say. Finally, we can call say to actually output and sayhi_ The results are the same. This is a feature of Python, in which functions are also objects.
Then, we can pass the function itself as a parameter to the function. This statement may be a bit tongue twister. Let's look at the code:
def sayhello_to(name): print('Hello, %s' %s) def talk_to_speculatecat(func): func('speculatecat') talk_to_speculatecat(sayhi_to) >> Hi, speculatecat --------------------------------- talk_to_speculatecat(sayhello_to) >> Hello, speculatecat
As you can see, we define two greeting functions: sayhi_to and sayhello_to, and then pass these two functions as parameters to talk_to_speculatecat, you can get different outputs.
In addition to passing function objects as parameters, functions can also be nested. Please see the following code:
def meet_speculatecat(action): print('Meet speculatecet!\n') def talk_to_speculatecat(action): action('speculatecat') return talk_to_speculatecat(action) meet_speculatecat(sayhello_to) >> Meet speculatecat! Hello speculatecat
The function of the above code is also very simple, that is, to call meet_ The speculatecat function will print a speculatecat first, and then call the talk function. Here is how Python calls functions within functions.
This may raise questions about why return talk is used at the end of the function_ to_ speculatecat(action) ? In terms, this kind of operation that takes the function object as the return value of the function is called closure. The details of closure will be described in a special chapter later. It can be understood simply as follows: the function will not take effect until it is called after it is declared. You can refer to the following code, and its effect is the same as that of the above code:
def meet_speculatecat2(action): print('Meet speculatecat, again!') def talk(action): action('speculatecat') talk(action) meet_speculatecat2(sayhi_to) >> Meet speculatecat, again! Hi, speculatecat
So far, we have tried and demonstrated four features of Python functions:
-
Functions are also objects. Function objects can be assigned to a variable, and functions can be called by calling this variable
-
A function object can be passed into another function as a parameter
-
The function can be redefined inside the function
-
The function object itself can also be used as the return value of the function. The operation of using the function object as the return value of the function is called closure
After we understand the above features, we can write our first decorator:
def deco(func): def wrapper(): print('wrapper of decorator') func() return wrapper def sayHello(): print('Hello speculatecat!') sayHello = deco(sayHello) sayHello() >> wrapper of decorator Hello speculatecat!
The above code is the most basic decorator. Here, sayHello will be called by wrapper(), and the outermost deco is a decorator, which can change the final behavior of sayHello without modifying the sayHello function itself.
@Use of grammar sugar
In our actual use, we don't need to assign the decorator to a variable and call it again. Python provides us with the syntax sugar of @ decorator. We still use the above code:
def deco(func): def wrapper(): print('wrapper of decorator') func() return wrapper @deco def sayHello(): print('Hello speculatecat!') sayHello() >> wrapper of decorator Hello speculatecat
We pass @ + the function name of the decorator, and then write the function we need to use the decorator. The result of the above code is the same as that of our previous code without @ syntax sugar.
Decorator with parameters and custom parameters
In the above code example, our function does not use parameters, but in fact, our decorator can take parameters. We then add the incoming parameters on the basis of the above code functions. The specific codes are as follows:
def deco(func): def wrapper(name): print('wrapper of decorator') func(name) return wrapper @deco def sayHello_to(name): print('Hello %s' % name) sayHello_to('speculatecat') >> wrapper of decorator Hello speculatecat
As shown in the example above, if I want to pass a parameter, I only need to add a parameter to the inner function wrapper of the inner decorator and add a parameter when func is called. Here is the use method with only one parameter. What if our decorated function has multiple parameters? Refer to the following code for specific code:
def mult_deco(func): def wrapper(*args, **kwargs): print('wrapper of mult decorator') func(*args, **kwargs) return wrapper @mult_deco def send_message(name, message): print('Send message: %s to %s' % (message, name)) send_message('speculatecat', 'How are you?') >> wrapper of mult decorator Send message: How are you? to speculatecat
It can be seen that the method using multiple parameters is similar to the above single parameter, except that the parameter is replaced with * args, **kwargs means that any number and type of parameter forms are accepted.
These two examples about parameters are both about the modified function, but recall the flash framework we mentioned at the beginning of the following article. The implementation is @ app Route ('/'), whose parameters are passed on the decorator. The following will demonstrate how to use the decorator with custom parameters.
Here, we demonstrate the use of custom parameter decorators by imitating the origin of flash. However, due to the length, we will not realize the complete function. We only use decorators to bind the PATH to a PATH variable. The specific codes are as follows:
Path = dict() def route(path): def deco(func): if path not in Path: Path[path] = func def wrapper(): print('Route %s' % path) func() return wrapper return deco @route('/') def home(): print('home pages') @route('/admin') def admin(): print('admin pages') def browser(path): if path not in Path: print('404 - Not found pages') else: return Path[path]() if __name__ == '__main__': print(Path) browser('/') browser('/admin') browser('user') >> {'/': <function home at 0x10105a8b0>, '/admin': <function admin at 0x10105a9d0>} home pages admin pages 404 - Not found pages
The above code defines a route modifier first. Its function is to bind the paths and their functions that do not exist in the Path dictionary to the Path dictionary. Then define two display functions admin and home, and finally a browser function to simulate our normal access to web pages. Then when we run the program, we can see that the browser function will display different contents according to different paths. When we receive a Path that does not exist, it will return an output that 404 cannot find the page.
The code here will have a trick, that is, in the code, we do not perform a binding action, or even call home admin or route. Why do we bind / and admin in our Path? This is how Python calculates the decorator. We will explain this in detail later.
Use @ functools Wrap() preserves the meta information of the original function
The modifier we used in the above example has a disadvantage, that is, the function to be modified__ name__ __doc__ Will be covered. Take the following example:
def deco(func): def wrapper(name): print('wrapper of decorator') func(name) return wrapper @deco def sayHello_to(name): print('Hello %s' % name) print(sayHello_to.__name__) >> wrapper
As you can see, our sayHello function__ name__ It has been replaced by wrapper. If I want to keep the meta information of the original function, we can use @ functools Wrap is a built-in decorator. The specific codes are as follows:
import functools def deco(func): @functools.wraps(func) def wrapper(name): print('wrapper of decorator') func(name) return wrapper @deco def sayHello_to(name): print('Hello %s' % name) print(sayHello_to.__name__) >> sayHello_to
We can see from the results of the code that @ functools. Is used After wraps built-in decorator, sayhello_ The meta information of to has been completely copied.
Class decorator
Decorators can be used not only on functions, but also on classes. If this kind of decorator is used__ call__ Whenever an instance of a class is called__ call__ () will be executed. Here's the code:
class Count: def __init__(self, func): print('execute __init__') self.func = func self.num_calls = 0 def __call__(self, *args, **kwargs): print('execute __call__') self.num_calls += 1 print('num of calls is : %s' % self.num_calls) return self.func(*args, **kwargs) def print_num_call(self): self.func() print('print num of calls is : %s' % self.num_calls) self.num_calls += 1 @Count def example(): print('Hello example func') def example2(): print('Hello example func2') if __name__ == "__main__": print('*' * 40 ) print('Script running...') example() c = Count(example2) c.print_num_call() example() c.print_num_call() >> execute __init__ **************************************** Script running... execute __call__ num of calls is : 1 Hello example func execute __init__ Hello example func2 print num of calls is : 0 execute __call__ num of calls is : 2 Hello example func Hello example func2 print num of calls is : 1
When using a class decorator, there is a trick, which is called by the decorating code__ call__ , But classes are not instantiated. You can see from the code that when we first call example(), we create a Count Object c and then call c's print counting function. Here we see that the c count after instantiation is 0. Then call example() again and you can see that the count will increase. This confirms that the decorated function will not instantiate the class, but directly execute the call__ call__ To modify the class variable num_calls .
Nesting of multiple decorators
The above examples all use a single decorator, and multiple decorators can be superimposed. Please see the following examples:
import functools def deco1(func): @functools.wraps(func) def wrapper(*args, **kwargs): print('running deco1...') func(*args, **kwargs) return wrapper def deco2(func): @functools.wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) print('running deco2...') return wrapper @deco1 @deco2 def say_hello_to(name): print('Hello, %s' % name) say_hello_to('speculatecat') >> running deco1... Hello, speculatecat running deco2...
Using multiple decorators will be applied to the decorated functions in order. The above code is essentially equivalent to say_hello_to = deco1(deco2(say_hello_to)), so you can see that the contents of wraps in the output result will appear in turn, and say_ hello_ To was executed only once.
Advanced
In the basic part above, we introduced the basic usage of the decorator, including how to use @ syntax sugar, how to use the decorator with parameters, and how to use @ functools Wraps to retain the meta information of the sleeve decoration function, as well as the less commonly used class decorators and the nested use of multiple decorators.
This advanced chapter mainly introduces how Python calculates decorators and further understands the characteristics of closure functions.
Two features of Python decorator
As we have already known in the previous pages, the first feature of Python decorator is that it can replace the decorated function with other functions.
However, in the above routing example imitating flash, there is no active call in the code, but the function of the display content of the Path has been automatically bound in the Path. This is the second feature of the Python decorator - the decorator will be executed immediately when the module is loaded.
For ease of understanding, please see the following code:
registry = [] def register(func): print('running register(%s)' % func) registry.append(func) return func @register def func1(): print('This is func1()!') @register def func2(): print('This is func2()') def func_no_deco(): print('This is func without decoator!') def main(): print('*' * 10 + 'Main Running!' + '*' * 10) print('registry -> ', register) func1() func2() func_no_deco() if __name__ == '__main__': main() >> running register(<function func1 at 0x105369820>) running register(<function func2 at 0x1053698b0>) **********Main Running!********** registry -> <function register at 0x1052cc040> This is func1()! This is func2() This is func without decoator!
We can see from the above code that the decorator will run first at the beginning of the program running, so you can see in the output that ********* Main Running********** register has been running before. This explains why in the previous example of imitation routing, the inner part in Path can be automatically bound.
This is the second feature of the Python decorator: the decorator is executed immediately when the module is loaded.
From decorator to closure
In our basic article example, we mentioned closures. Because our decorators usually have nested functions, the problem of closure will always be around. I believe that beginners have heard of closures, but they don't know them well. The following will try to introduce the connotation of the following closures in a relatively simple way. Due to my limited level, the following contents will learn from the code examples of the book "fluent Python". This book is a very excellent Python technology book. It deeply explains many contents and knowledge points that are not studied deeply in daily use of python, but they are indeed very key. However, the contents of this book may not be so friendly to beginners, But if you have a certain understanding of python, you should get a lot from reading this book.
Scope
Before we start talking about closures, let's take a look at the variable scope of a function. Let's look at the code first:
def func_print_a(a): print(a) print(b) func_print_a(42) >> 42 Traceback (most recent call last): File "...", line 5, in <module> func_print_a(42) File "...", line 3, in func_print_a print(b) NameError: name 'b' is not defined
This code is very simple, which is to print the input variable A. because the variable b is not defined, an error will be reported. What happens if we define a b variable outside the function?
def func_print_a(a): print(a) print(b) b = 0 func_print_a(42) >> 42 0
We can see that we define a b variable outside the function. There is no b variable inside the function, but the function can automatically obtain the global variable b outside the function. Next, let's take another example:
def func_print_a(a): print(a) print(b) b = 100 b = 0 func_print_a(42) >> 42 Traceback (most recent call last): File "...", line 7, in <module> func_print_a(42) File "...", line 3, in func_print_a print(b) UnboundLocalError: local variable 'b' referenced before assignment
Func before_ print_ Add a sentence at the end of function a, assign 100 to b, call the function again, and find that our function reports an error again. This is because when the b variable is declared inside the function, the function will get the internal (local variable) of the function instead of the global variable. However, because our print statement is before the assignment of b, we will see the error report of the above code. If we want to print the global variable first, and then assign a new value to this variable, the operation is also very simple, just add the global keyword.
def func_print_a(a): global b print(a) print(b) b = 100 b = 0 func_print_a(42) print(b) >> 42 0 100
Here, we have probably understood the scope of Python functions. If you want to have a deeper understanding of this topic, you can refer to it Introduction to MIT 6.0001 computer In this video public lesson, the second half of lesson 4 will explain the function scope in detail (the subtitle of the course is: scope Essentials), which has a very good explanation with pictures and text.
Use and advancement of closures
Closure function can realize the functions realized by some classes to a certain extent. The following will demonstrate the closure implementation method for calculating the moving average in fluent Python. The book also demonstrates the implementation method of object-oriented classes, but this article will not list them. Those interested can learn from the book.
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager avg = make_averager() print(avg(50)) print(avg(100)) print(avg(200)) >> 50.0 75.0 116.66666666666667
It can be seen that we call avg three times, and all our newly added values can be saved in series. Why? After the normal function is called, shouldn't the variables in the function be destroyed automatically?
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager avg = make_averager() print(avg.__code__.co_varnames) #1 print(avg.__code__.co_freevars) #2 print(avg.__closure__) #3 print(dir(avg.__closure__[0])) #4 print(avg.__closure__[0].cell_contents) #5 avg(50) print(avg.__closure__[0].cell_contents) #6 avg(100) print(avg.__closure__[0].cell_contents) #7 >> ('new_value', 'total') #1 ('series',) #2 '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents'] #3 (<cell at 0x100e87eb0: list object at 0x100e2c7c0>,) #4 [] #5 [50] #6 [50, 100] #7
From the above code, we can know that we can learn from the function__ code__ Property, the local variable new is saved_ Value total and the name of the free variable series. At the same time, in__ closure__ The corresponding is bound in__ code__. co_ The value of freevars. Every time we call avg(), new_value will add the new value to the free variable series, and the value of the free variable series will actually be stored in__ closure__[0].cell_contents.
So can we use closures as classes? The answer is yes, but not entirely. See the following code:
def make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager avg = make_averager() print(avg(50)) print(avg(100)) print(avg(200)) >> Traceback (most recent call last): File "...", line 13, in <module> print(avg(50)) File "...", line 6, in averager count += 1 UnboundLocalError: local variable 'count' referenced before assignment
Why does the code that just can run normally report an error if it only modifies the calculation method? Like the previous code, let's take a look at the local variables and free variables bound by the function:
def make_averager(): count = 0 total = 0 def averager(new_value): count += 1 total += new_value return total / count return averager avg = make_averager() print(avg.__code__.co_varnames) print(avg.__code__.co_freevars) print(avg.__closure__) >> ('new_value', 'count', 'total') () None
We can get__ code__ And__ closure__ Property to view the condition of the variable. From the output results, we can know that this time, count and total in our function are not free variables, but become local variables. Therefore, it is obvious that an error will be reported when running the code.
The reason for this problem is that the list we used in the previous code is variable type objects, while in Python, numbers (int float), characters (str) and tuples (tupe) are immutable objects. If immutable objects are used, python will create them as local variables instead of free variables, so they will not be saved in zai closures.
If you have to realize the above functions, what should you do? The method is also very simple. You can use the nonlocal keyword. The specific codes are as follows:
def make_averager(): count = 0 total = 0 def averager(new_value): nonlocal count, total count += 1 total += new_value return total / count return averager avg = make_averager() print(avg(50)) #1 print(avg(100)) #2 print(avg(200)) #3 print(avg.__code__.co_varnames) #4 print(avg.__code__.co_freevars) #5 print(avg.__closure__) #6 print(avg.__closure__[0].cell_contents) #7 print(avg.__closure__[1].cell_contents) #8 >> 50.0 #1 75.0 #2 116.66666666666667 #3 ('new_value',) #4 ('count', 'total') #5 (<cell at 0x104887ee0: int object at 0x10460e970>, <cell at 0x104887eb0: int object at 0x1046b6850>) #6 3 #7 350 #8
After we add the keyword nonlocal, the variables count and total become free variables__ closure__ Attribute also corresponds to the values of these two variables, and the function function can also be realized normally.
Application article
After the introduction of the above basic chapter and advanced chapter, I believe we have a certain understanding of decorators. Next, we will consolidate the application of the following decorators through several actual case codes.
Authentication
The following takes a simple code for an administrator to obtain information as an example to demonstrate how to use the decorator to complete the function of permission management. It should be noted that in order to make the code simple and easy to understand, the demo code is not rigorous, but shows how to operate according to different user permissions through the decorator. The code is as follows:
import functools def auth_admin(func): @functools.wraps(func) def wrapper(*args, **kwargs): role = args[0]['role'] if role == 'admin': return func(*args, **kwargs) else: raise Exception('Permission Denied') return wrapper @auth_admin def get_admin_message(user): print('Get Message from:Username: %s, Role: %s' % (user['name'], user['role'])) if __name__ == "__main__": user = {'role': 'admin', 'name': 'speculatecat'} user_nomal = {'role': 'user', 'name': 'nomal_user'} get_admin_message(user) get_admin_message(user_nomal) >> Get Message from:Username: speculatecat, Role: admin Traceback (most recent call last): File "...", line 23, in <module> get_admin_message(user_nomal) File "...", line 10, in wrapper raise Exception('Permission Denied') Exception: Permission Denied
In the example code, the role parameter is hard coded in the decorator for verifying permissions, which will not be done in the actual code. However, this is designed for the convenience of demonstration. You can see that after we use the decorator, we don't need to get_ admin_ Make any logical judgment of user permission in the message function, which makes our code more concise and easy to understand.
journal
In addition to permission verification, logging is also a common place of decorators. The following will demonstrate the logging function of recording the running time of a function. The code is as follows:
import time import functools def log(func): @functools.wraps(func) def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - t0 name = func.__name__ arg_lst = [] if args: arg_lst.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('(%0.8fs) %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked @log def test_func(sleep_time): time.sleep(sleep_time) return True test_func(3.14) >> corator/article_009_log_demo.py (3.14534538s) test_func(3.14) -> True
In the above code, the decorator obtains the function name, incoming parameters, return value and other information of the decorated function, calculates the total running time of the function, and finally formats the output result. Through this log decorator, we only need to use @ syntax sugar on the functions that need logs.
cache
At the end of this part, let's introduce the following LRU_ Use of cache. Before introducing, let's take a look at the following time required for our computer to run a calculation of Fibonacci Number:
import time import functools def log(func): @functools.wraps(func) def clocked(*args, **kwargs): t0 = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - t0 name = func.__name__ arg_lst = [] if args: arg_lst.append(', '.join(repr(arg) for arg in args)) if kwargs: pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())] arg_lst.append(', '.join(pairs)) arg_str = ', '.join(arg_lst) print('(%0.8fs) %s(%s) -> %r' % (elapsed, name, arg_str, result)) return result return clocked @log def fibonacci(n): if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1) if __name__ == '__main__': print(fibonacci(25)) >> 0.00000013s) fibonacci(0) -> 0 (0.00000013s) fibonacci(1) -> 1 (0.00000412s) fibonacci(2) -> 1 (0.00000013s) fibonacci(1) -> 1 (0.00000013s) fibonacci(0) -> 0 (0.00000013s) fibonacci(1) -> 1 (0.00000371s) fibonacci(2) -> 1 (0.00000750s) fibonacci(3) -> 2 (0.00001504s) fibonacci(4) -> 3 (0.00000012s) fibonacci(1) -> 1 (0.00000012s) fibonacci(0) -> 0 (0.00000013s) fibonacci(1) -> 1 ... (0.01913079s) fibonacci(15) -> 610 (0.02132400s) fibonacci(16) -> 987 (0.04015479s) fibonacci(17) -> 1597 (0.06254508s) fibonacci(18) -> 2584 (0.08707908s) fibonacci(19) -> 4181 (0.16693542s) fibonacci(20) -> 6765 (0.27160862s) fibonacci(21) -> 10946 (0.41941350s) fibonacci(22) -> 17711 (0.71603742s) fibonacci(23) -> 28657 (1.20284050s) fibonacci(24) -> 46368 (2.04485879s) fibonacci(25) -> 75025 75025
It can be seen that the calculation takes more than 2 seconds to complete, and many repeated calls can be seen in the process of output. At this time, our lru_cache can be used to implement caching to improve performance.
@functools.lru_cache() @log def fibonacci(n): if n < 2: return n return fibonacci(n - 2) + fibonacci(n - 1) >> (0.00000042s) fibonacci(1) -> 1 (0.00000033s) fibonacci(0) -> 0 (0.00000892s) fibonacci(2) -> 1 (0.00005983s) fibonacci(3) -> 2 (0.00000067s) fibonacci(4) -> 3 (0.00006875s) fibonacci(5) -> 5 (0.00000058s) fibonacci(6) -> 8 (0.00007733s) fibonacci(7) -> 13 (0.00000046s) fibonacci(8) -> 21 (0.00008596s) fibonacci(9) -> 34 (0.00000050s) fibonacci(10) -> 55 (0.00009442s) fibonacci(11) -> 89 (0.00000046s) fibonacci(12) -> 144 (0.00010279s) fibonacci(13) -> 233 (0.00000054s) fibonacci(14) -> 377 (0.00011092s) fibonacci(15) -> 610 (0.00000046s) fibonacci(16) -> 987 (0.00012158s) fibonacci(17) -> 1597 (0.00000046s) fibonacci(18) -> 2584 (0.00012967s) fibonacci(19) -> 4181 (0.00000046s) fibonacci(20) -> 6765 (0.00013713s) fibonacci(21) -> 10946 (0.00000046s) fibonacci(22) -> 17711 (0.00014613s) fibonacci(23) -> 28657 (0.00000046s) fibonacci(24) -> 46368 (0.00015433s) fibonacci(25) -> 75025 75025
Other codes remain unchanged. I just need to add @ functools lru_ Cache(). From the output results, the running completion time only takes 0.00015 seconds, which is much faster than the code just now. And there will be no repeated calls in the code this time.
summary
This paper introduces Python decorator from three dimensions: basic, advanced and application. Although the content is relatively long, it should be a more comprehensive and in-depth introduction to the content of the decorator. This article refers to many excellent tutorials and articles, among which "geek time - Python core technology and practice" is a very excellent introductory tutorial. The contents of this series are familiar, easy to understand and quite enlightening. Both novices and programmers with certain programming experience are worth reading. Another important reference is "fluent Python". I have bought this book for a long time, but at the beginning, because the foundation is not solid, and the content of this book is more in-depth in details and principles, I gave up after reading it many times. Recently, I picked it up again and found that many contents are like treasures. That's why we have to combine some basics and the content of this book to further study.
Even if this article is a technical sharing article, it is also my learning notes. Maybe because of their limited level and insufficient cognition, there will be mistakes and omissions. I hope readers can forgive and understand more. At the same time, you are also welcome to correct your mistakes and discuss them together.
At first, I thought it was relatively simple to complete this article, but in fact, it took a lot of time from the beginning of writing to the completion of the article. I deeply feel that it is not easy to create. I hope the article can bring you help and inspiration. If you think the article is useful to you, please don't be stingy to like, collect, pay attention to and share. Your effort is the source of motivation for me to continue my creation.
reference material
[MIT 6.0001]
[fluent Python ISBN: 9787115454157]
[geek time - Python core technology and practice]
[in depth understanding of Python features ISBN: 9787115511546]