Get a quick look at decorators in Python

  • 2020-06-23 01:02:34
  • OfStack

Some concepts to understand

To understand decorators in Python, I'd like to start with the basics:

Decorator pattern: The decorator pattern can simply be understood as "adding a property to a function or class without changing the original internal implementation." This allows us to abstract away some of the business-neutral, generic code and attach it as a decorator to the function or class that needs it. The idea of aspect-oriented programming is that a decorator should be a section.

Function is citizen 1: this means that a function can be used as a normal variable 1. In Python, functions can be assigned to variables, taken as arguments to other functions, or returned as values of other functions.

Closures: We all know that local scopes can refer to variables in the global scope. Similarly, when a function has other functions defined inside it, the inner function can use variables in the scope of the outer function.

Start with the simplest decorator

Having understood the concepts above, let's try to implement a simple decorator using these features.

First l clear requirements, we sometimes need to print a corresponding log when a function call, although can through in all need to print log function code embedded in the code of print log to achieve, but this method has not only increased a lot of duplicate code, but has nothing to do with the business of the code to be embedded in the business code increases the overall coupling. Therefore, we need to implement a decorator that prints a log of function call behavior when a function call is made.

If we have the following function, foo, which represents a specific business function:


def foo():
  print('in function foo')

We envisage adding print-logging to the function foo by calling foo = deco(foo) without affecting its original business. In this scenario, the decorator deco would also be a function that takes another function as input and returns a new, decorated function. In Python, we can write as follows:


def deco(func): #  receive 1 A function as a parameter 
  def new_func():
    print(f'[log] run function {func.__name__}') #  The use of Python3.6 Formatted string of 
    func() #  A closure that USES variables from an external function in an internal function 
  return new_func #  Returns the new function as the return value 

Try the following effect:


>>> foo = deco(foo)
>>> foo()
[log] run function foo
in function foo

Okay, so far we've implemented the simplest decorator possible! In the above code, the decorator deco takes an arbitrary function as an argument, constructs another function internally, and USES the closure feature to call the function func in the decorator deco local scope in the new function.

The magic of @

Each time we have to assign a new value to a function that needs to be decorated, the number of 10,000 functions or decorators increases, and manually writing assignments and function calls becomes very cumbersome. Is there a more elegant way to write it in Python? The answer is yes, you only need 1 @ sign.

In Python, when we need a decorator:


def deco(func):
  def new_func():
    print(f'[log] run function {func.__name__}')
    func()
  return new_func

@deco
def foo():
  print('in function foo')

This is where we omit the assignment of the function and simply decorate the line 1 above the definition of foo with @deco. Try running 1:


>>> foo()
[log] run function foo
in function foo

Isn't that amazing? There is no magic to this, it's just that Python adds the logic of foo=deco(foo) to the code that defines the function.

Decorators also want parameters

The above code prints the log before the business function call, but what if we need to print a custom message after the business code has executed? We have to make our decorator accept custom parameters.

As mentioned above, all Python does is assign the result of calling deco(func) to its decorated function when @deco is written. Following this logic, when we need a decorator with arguments, the code should be @deco ('some message') and Python assigns the result of calling deco(msg)(func) to foo. Then it's easy to nest a layer of functions on top of the above code:


def deco(msg):
  def inner_deco(func):
    def new_func():
      print(f'[log] run function {func.__name__}')
      func()
      print(f'[log] {msg}')
    return new_func
  return inner_deco

@deco('some message')
def foo():
  print('in function foo')

Execute 1 next try:


>>> foo()
[log] run function foo
in function foo
[log] some message

A decorator that does not support a decorated function with arguments is not a good decorator

The above code is problematic because we only consider the case when the function foo has no arguments. If the 10,000 function foo has arguments, the decorator will lose the argument information, which is not the case for a qualified decorator. Therefore, we use *args and **kwargs in Python to enable the decorated function to support passing in any parameters:


def deco(msg):
  def inner_deco(func):
    def new_func(*args, **kwargs):
      print(f'[log] run function {func.__name__}')
      func(*args, **kwargs)
      print(f'[log] {msg}')
    return new_func
  return inner_deco

@deco('some message')
def foo(a, b=None):
  print('in function foo')
  print(f'a is {a} & b is {b}')

In this way, no matter what the parameter list of foo is, there will be no problem:


>>> foo('hello')
[log] run function foo
in function foo
a is hello & b is None
[log] some message

A decorator that does not support a decorated function that does not have a return value is not a good decorator

Remember, none of the functions we've written so far return a value. What if foo does? I think you have the answer in mind:


def deco(msg):
  def inner_deco(func):
    def new_func(*args, **kwargs):
      print(f'[log] run function {func.__name__}')
      rlt = func(*args, **kwargs)
      print(f'[log] {msg}')
      return rlt
    return new_func
  return inner_deco

@deco('some message')
def foo(a, b=None):
  print('in function foo')
  print(f'a is {a} & b is {b}')
  return 'ok'

Since the decorator has other operations after the original function has executed, the return value should be stored until the decorator's logic completes before the final result is returned. This is our final decorator!


def deco(func): #  receive 1 A function as a parameter 
  def new_func():
    print(f'[log] run function {func.__name__}') #  The use of Python3.6 Formatted string of 
    func() #  A closure that USES variables from an external function in an internal function 
  return new_func #  Returns the new function as the return value 
0

Is there a more sexy operation?

Of course! I write the title so difficult will not have?

In Python, you can use classes as decorators:


def deco(func): #  receive 1 A function as a parameter 
  def new_func():
    print(f'[log] run function {func.__name__}') #  The use of Python3.6 Formatted string of 
    func() #  A closure that USES variables from an external function in an internal function 
  return new_func #  Returns the new function as the return value 
1

def deco(func): #  receive 1 A function as a parameter 
  def new_func():
    print(f'[log] run function {func.__name__}') #  The use of Python3.6 Formatted string of 
    func() #  A closure that USES variables from an external function in an internal function 
  return new_func #  Returns the new function as the return value 
2

The advantage of doing this is that you can use classes to better manage arguments and call logic. Isn't this much clearer than the previous 3 levels of function nesting?

In Python, you can also decorate a class with a decorator, like this:


def add_doc(doc):
  def deco(cls):
    cls.__doc__ = doc
    return cls
  return deco

@add_doc('this is the doc of Cls')
class Cls(object):
  pass

Here's what it looks like:


def deco(func): #  receive 1 A function as a parameter 
  def new_func():
    print(f'[log] run function {func.__name__}') #  The use of Python3.6 Formatted string of 
    func() #  A closure that USES variables from an external function in an internal function 
  return new_func #  Returns the new function as the return value 
4

The above code is just an example of how a decorator decorates a class, not that it should be used in the real world. In most cases, we should extend our classes through inheritance rather than decoration.

How to use adornment ingeniously specific should rely on everybody to develop his imagination.

conclusion

That's all for a quick understanding of the decorators in Python, hoping to help you. Interested friends can continue to refer to other related topics in this site, if there is any deficiency, welcome to comment out. Thank you for your support!


Related articles: