Python decorator for deep understanding

  • 2020-05-26 09:36:58
  • OfStack

Before I talk about the Python decorator, I want to give an example. Although it is a little dirty, it is very close to the topic of decorator.

Everyone has underwear is used to cover up the main function, but in the winter it can not protect us from the cold wind, how to do? So one of the things that we thought of was to make the underwear 1, make it thicker and longer, so that 1, it not only has a shaming function, but it also provides warmth, but the problem is that when we made the underwear into pants, it still has a shaming function, but essentially it's no longer a real pair of underwear. So the clever people invented trousers, in the premise of not affecting underwear, directly put trousers on the outside of the underwear, so underwear or underwear, after having trousers baby is not cold. Decorator is like what we say here long pants, below the premise that does not affect underwear function, provided the effect that keeps warm to our body.

Before talking about the decorator, we should first understand one thing. The functions in Python are different from those in Java and C++. The functions in Python can be passed to another function as parameters like the ordinary variable 1, for example:


def foo():
  print("foo")

def bar(func):
  func()

bar(foo)

Let's get back to the point. The decorator is essentially an Python function or class that allows other functions or classes to add additional functionality without any code changes. The return value of the decorator is also a function/class object. It is often used in scenarios with aspect requirements, such as insert logs, performance testing, transaction processing, caching, permission validation, etc. Decorators are a great design solution to this problem. With decorators, we can pull out a lot of identical code that has nothing to do with the functionality itself and reuse it. In summary, the role of a decorator is to add additional functionality to an existing object.

Let's start with a simple example, although the actual code might be a lot more complex than that:


def foo():
  print('i am foo')

Now I have a new requirement, I want to log the execution of the function, so I add the log code in the code:


def foo():
  print('i am foo')
  logging.info("foo is running")

What if the functions bar() and bar2() have similar requirements? I'm going to write an logging in the bar function, right? This results in a lot of identical code. To reduce duplication, we can do this by redefining a new function that handles logs exclusively and executes the actual business code after the logs are processed


def use_logging(func):
  logging.warn("%s is running" % func.__name__)
  func()

def foo():
  print('i am foo')

use_logging(foo)

Do logically is no problem, the function is achieved, but we call is no longer calls the real business logic foo function, but is replaced by use_logging function, the structure is destroyed the original code, now we have to always take the original foo function passed as a parameter to use_logging function, so is there a better way? Of course, the answer is decorators.

Simple decorator


def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
return func()  #  the  foo  When passed in as a parameter, execute func() It's like executing foo()
return wrapper

def foo():
  print('i am foo')

foo = use_logging(foo) #  Because the decorator  use_logging(foo)  Returns the time function object  wrapper , this statement is equivalent to  foo = wrapper
foo()          #  perform foo() It's like executing  wrapper()

use_logging is a decorator, it is a normal function, it is the implementation of the real business logic function func wrapped in it, it looks like foo is decorated by use_logging 1, use_logging return a function, the name of this function is wrapper. In this example, when the function enters and exits, it is called a cross section, and this type of programming is called aspect oriented programming.

@ syntactic sugar

If you've been with Python for a while, you're probably familiar with the @ symbol 1, which is the syntactic sugar of the decorator, placed at the beginning of the function definition so that you can omit the last step of re-assignment.


def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
  print("i am foo")

foo()

As shown above, with @, we can leave out the sentence foo = use_logging(foo) and call foo() directly to get the desired result. You see, the foo() function doesn't have to change anything, just add decorators to the definition, and when we call it, it's the same as before, and if we have other functions like this, we can continue to call decorators to decorate the function, without having to modify the function or add a new wrapper. In this way, we have improved the reusability of the program and increased the readability of the program.

The reason why decorators are so easy to use in Python is that Python's functions can be passed as arguments to other functions like ordinary object 1, can be assigned to other variables, can be returned as return values, and can be defined in another function.

* args, * * kwargs

One might ask, what if my business logic function foo needs parameters? Such as:


def foo(name):
  print("i am %s" % name)

We can specify parameters when defining the wrapper function:


def wrapper(name):
    logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper

The parameters defined by the foo function can then be defined in the wrapper function. At this point, another question is, what if the foo function takes two arguments? What about the three parameters? What's more, I may pass many. When the decorator does not know exactly how many parameters foo has, we can use *args instead:


def wrapper(*args):
    logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper

So, no matter how many parameters foo defines, I can pass them to func completely. This does not affect the business logic of foo. At this point, the reader may ask, what if the foo function also defines 1 keyword parameter? Such as:


def foo(name, age=None, height=None):
  print("I am %s, age %s, height %s" % (name, age, height))

At this point, you can specify the wrapper function keyword function:


def foo():
  print('i am foo')
0

Parameter decorator

Decorators also have greater flexibility, such as a decorator with parameters, which in the decorator call above receives only one parameter that is the function foo that performs the business. The decorator's syntax allows us to provide additional parameters, such as @decorator (a), when invoked. This provides greater flexibility for writing and using decorators. For example, we can specify the level of logging in the decorator, because different business functions may require different levels of logging.


def foo():
  print('i am foo')
1

The above use_logging is a decorator that allows parameters. It actually encapsulates a function of the original decorator and returns a decorator. We can think of it as a closure with a parameter. When we call it with @use_logging (level= "warn"), Python is able to find this 1 layer of encapsulation and pass it to the decorator's environment.

@use_logging (level= "warn") is equivalent to @decorator

Class decorator

Right, decorator can be a function not only, still can be a class, compare function decorator, class decorator has agile degree big, tall cohesion, encapsulation to wait for an advantage. The use of the class decorator relies primarily on the class's s s 126en__ method, which is invoked when attaching the decorator to the function using the @ form.


def foo():
  print('i am foo')
2

functools.wraps

The use of a decorator greatly multiplexes the code, but one drawback is that the meta information of the original function is missing, such as the function docstring, s 134en__, the parameter list, see the example first:


#  A decorator 
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__   #  The output  'with_logging'
print func.__doc__    #  The output  None
return func(*args, **kwargs)
return with_logging

#  function 
@logged
def f(x):
"""does some math"""
return x + x * x

logged(f)

It is not difficult to find that the function f is replaced by with_logging, and of course its docstring, s 142en__ becomes the information of the function with_logging. Fortunately, we have functools.wraps, and wraps itself is a decorator. It can copy the meta information of the original function into the func function in the decorator, which makes the meta information of the func function in the decorator as well as the meta information of the original function foo 1.


def foo():
  print('i am foo')
4

Decorator sequence

A function can also define multiple decorators at the same time, such as:


def foo():
  print('i am foo')
5

It is executed from the inside out, calling the innermost decorator first, and calling the outermost decorator last, which is equivalent to


def foo():
  print('i am foo')
6

Thank you for reading, I hope to help you, thank you for your support of this site!


Related articles: