The Python decorator USES detail

  • 2020-06-07 04:44:50
  • OfStack

Python has a lot of powerful and intimate features, and if you were to make a top list, decorators would definitely be there.

The decorator for the first time, you will feel elegant and magical, but there is always a sense of distance when you want to personally realize, just like the ice beauty of boudoir 1. This is often because understanding decorators mixes up other concepts. When I lift the veil, you will see that the pure ornament is quite simple and straightforward.

The principle of decorator

Run an example of a decorator under the interpreter.
# make_bold is the decorator, omitted here


>>> @make_bold
... def get_content():
...  return 'hello world'
...
>>> get_content()
'<b>hello world</b>'

get_content, which is decorated by make_bold, is automatically wrapped by the b tag when it is called. So how do you do that? You can do it in 4 simple steps.

1. Functions are objects

Let's define an get_content function. In this case, get_content is also an object that can do all the operations of an object.


def get_content():
  return 'hello world'

It has id, it has type, it has value.


>>> id(get_content)
140090200473112
>>> type(get_content)
<class 'function'>
>>> get_content
<function get_content at 0x7f694aa2be18>

Like other objects 1 can be assigned to other variables.


>>> func_name = get_content
>>> func_name()
'hello world'

It can be passed as an argument or as a return value


>>> def foo(bar):
...   print(bar())
...   return bar
...
>>> func = foo(get_content)
hello world
>>> func()
'hello world'

2. Customize function objects

We can use class to construct function objects. ______en__ is the function object with the member function per ___, and it is called with each ___.


class FuncObj(object):
  def __init__(self, name):
    print('Initialize')
    self.name= name

  def __call__(self):
    print('Hi', self.name)

So let's call and see. As you can see, the function object is used in two steps: construct and call.


>>> fo = FuncObj('python')
Initialize
>>> fo()
Hi python

3. @ is a grammar candy

The @ of the decorator does nothing special, and you can do one without it, but it requires more code.


@make_bold
def get_content():
  return 'hello world'

#  The code above is equivalent to the code below 

def get_content():
  return 'hello world'
get_content = make_bold(get_content)

make_bold is a function that requires an input as a function object and a return value as a function object. The @ syntax sugar actually saves the last line of code above, making it more readable. With the decorator, each call to get_content actually calls the function object returned by make_bold.

4. Implement decorators with classes

Input is a function object, return is a function object, if the constructor of the class in step 2 changed to input is a function object, not exactly meet the requirements? Let's try implementing make_bold.


class make_bold(object):
  def __init__(self, func):
    print('Initialize')
    self.func = func

  def __call__(self):
    print('Call')
    return '<b>{}</b>'.format(self.func())

That's it. Let's see if it works.


>>> @make_bold
... def get_content():
...   return 'hello world'
...
Initialize
>>> get_content()
Call
'<b>hello world</b>'

Decorator successfully implemented! Isn't that easy?

Here's a look at the construction and invocation procedures highlighted earlier in 1. Let's get rid of the @ syntax sugar to make it easier to understand.
Construct, using the constructor object with the decorator, calling ___


def get_content():
  return 'hello world'
0

This is where it's all clear, the end of the flowers, you can close the page ~~~(if you just want to know how the decorator works)

Function version decorator

When you read the source code, you often see a decorator implemented with nested functions. Again, only 4 steps.

1. Function object initialization of def

It's easy to see when a function object implemented with class is constructed, but when is a function object defined with def constructed?
The global variable here has deleted the extraneous


>>> globals()
{}
>>> def func():
...   pass
...
>>> globals()
{'func': <function func at 0x10f5baf28>}

Unlike some compiled languages, the function is already constructed at startup. As you can see from the above example, it takes until def to construct a function object and assign a value to the variable make_bold.

This code looks a lot like the code below.


class NoName(object):
  def __call__(self):
    pass

func = NoName()

2. Nested functions

Python's functions can be nested.


def get_content():
  return 'hello world'
3

inner is defined within outer, so count outer as a local variable. The function object is not created until def inner executes, so each call to outer creates a new inner. As you can see below, inner is different each time it is returned.


def get_content():
  return 'hello world'
4

3. The closure

What's so special about nested functions? Because there are closures.


def get_content():
  return 'hello world'
5

The following experiments show that inner can access the local variable msg of outer.


def get_content():
  return 'hello world'
6

Closures have two characteristics
1. inner can access variables (local variables, function parameters) in the namespace of outer and its ancestor functions.
2. The call to outer has been returned, but its namespace is referenced by the returned inner object, so it will not be reclaimed yet.

For this part, you can learn the LEGB rules of Python.

4. Implement decorators with functions

The decorator requires that the input is a function object and the return value is a function object, so nested functions are perfectly adequate.


def make_bold(func):
  print('Initialize')
  def wrapper():
    print('Call')
    return '<b>{}</b>'.format(func())
  return wrapper

The usage is similar to the decorator 1 implemented by the class. You can remove the @ syntax sugar analysis and the timing of the construction and invocation.


>>> @make_bold
... def get_content():
...   return 'hello world'
...
Initialize
>>> get_content()
Call
'<b>hello world</b>'

Because the returned wrapper is still referenced, the func that exists in the make_bold namespace does not disappear. make_bold can decorate multiple functions, and wrapper does not confuse calls, because each call to make_bold creates a new namespace and a new wrapper.

This function is used to implement the decorator is also clear, the end of the flowers, you can close the page ~~~(following is the use of decoration FAQ)

Q&A

1. How to implement a decorator with parameters?

Decorators with parameters sometimes work exceptionally well. Let's look at an example.


def get_content():
  return 'hello world'
9

How do you do that? This has nothing to do with decorator syntax. Take away the @ syntax sugar and it becomes easy to understand.


@make_header(2)
def get_content():
  return 'hello world'

#  Is equivalent to 

def get_content():
  return 'hello world'
unnamed_decorator = make_header(2)
get_content = unnamed_decorator(get_content)

In the code above, unnamed_decorator is the real decorator, and make_header is a normal function whose return value is the decorator.

Look at the code for the implementation in 1.


def make_header(level):
  print('Create decorator')

  #  This part follows the usual decorator 1 Sample, just wrapper Variables are accessed through closures level
  def decorator(func):
    print('Initialize')
    def wrapper():
      print('Call')
      return '<h{0}>{1}</h{0}>'.format(level, func())
    return wrapper

  # make_header Return decorator 
  return decorator

Looking at the implementation code, the construction of the decorator and the timing of the invocation are clear.


>>> @make_header(2)
... def get_content():
...   return 'hello world'
...
Create decorator
Initialize
>>> get_content()
Call
'<h2>hello world</h2>'

2. How to decorate a function with parameters?

To get an organized understanding of decorators, the decorator function in the previous example was deliberately designed to be parametrized. Let's look at an example.


@make_bold
def get_login_tip(name):
  return 'Welcome back, {}'.format(name)

The most straightforward idea is to pass through the parameters of get_login_tip.


class make_bold(object):
  def __init__(self, func):
    self.func = func

  def __call__(self, name):
    return '<b>{}</b>'.format(self.func(name))

This is fine if the parameters of the decorated function are explicitly fixed. But make_bold is clearly not the case. It needs to decorate both get_content with no parameters and get_login_tip with parameters. That's when you need to have variable parameters.


class make_bold(object):
  def __init__(self, func):
    self.func = func
  def __call__(self, *args, **kwargs):
    return '<b>{}</b>'.format(self.func(*args, **kwargs))

Variable parameters are ideal when the decorator does not care about the parameters of the decorated function, or when the parameters of the decorated function are various. Variable parameters are not part of the syntax of decorators, and we won't go into that here.

3. Can 1 function be decorated with multiple decorators?

Is it legal to write it this way?


@make_italic
@make_bold
def get_content():
  return 'hello world'

Legal. The above code is equivalent to the following. Note the order of the 1 decorations.


def get_content():
  return 'hello world'
get_content = make_bold(get_content) #  First, decorate the one near the function definition 
get_content = make_italic(get_content)

4. functools. What does wraps do?

One of the more intimate aspects of the Python decorator is that it is transparent to the caller. The caller has no idea or need to know that the calling function is decorated. This allows us to give the function patch functionality without changing the caller's code at all.

To be transparent to the caller, the object returned by the decorator is disguised as the decorated function. The more similar the disguise, the smaller the difference for the caller. It is not enough to disguise the function name and arguments for some time, because the Python function object has some meta information that the caller may have read. To disguise even this meta-information, functools.wraps appears. It can be used to assign the called function to the function object returned by the decorator with each successive 259EN__, 260en__, 261en__, 262en__, 263en__.


import functools

def make_bold(func):
  @functools.wraps(func)
  def wrapper(*args, **kwargs):
    return '<b>{}</b>'.format(func(*args, **kwargs))
  return wrapper

Compare the effect in 1.


>>> @make_bold
... def get_content():
...   '''Return page content'''
...   return 'hello world'

#  Don't have to functools.wraps The results of the 
>>> get_content.__name__
'wrapper'
>>> get_content.__doc__
>>>

#  with functools.wraps The results of the 
>>> get_content.__name__
'get_content'
>>> get_content.__doc__
'Return page content'

You often don't know how the caller will use a decorator, so get in the habit of adding functools.wraps.

This time it's real. It's over. Scatter flowers


Related articles: