Decorators in Python are explained in detail

  • 2020-04-02 14:28:53
  • OfStack

This article illustrates the use of decorators in Python. Share with you for your reference. Specific analysis is as follows:

Let's start with a question above stackoverflow, using the following code:

@makebold
@makeitalic
def say():
   return "Hello"

Print out the following output:

< b > < i. > Hello! < i. > < / b >
What would you do? The answer is:

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped
 
def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped
 
@makebold
@makeitalic
def hello():
    return "hello world"
 
print hello() ## return <b><i>hello world</i></b>

Now let's look at some of the most basic ways to understand Python decorators.

Decorators are a well-known design pattern that is often used in scenarios where there are sectional requirements, such as insert logging, performance testing, transaction processing, and so on. Decorators are a great design solution to this type of problem, and with them we can pull out a lot of identical code from functions that has nothing to do with the functionality itself and continue to reuse it. In a nutshell, the role of decorators is to add additional functionality to existing objects.

1.1. Where does the demand come from?

The definition of decorator is very abstract, let's look at a small example.

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

This is a pretty boring function, right. But all of a sudden a much more boring guy, we call him Mr. B, says I want to see how long it takes to execute this function, okay, so here's what we can do:
import time
def foo():
    start = time.clock()
    print 'in foo()'
    end = time.clock()
    print 'used:', end - start
 
foo()

Good. The functionality looks impeccable. However, B suddenly doesn't want to look at this function, he is more interested in another function called foo2.

What to do? If you copy the above new code into foo2, it's a big no-no. And, what if B continues to look at other functions?

1.2 with the same should not change, is also change

Remember, the function is first class citizens in Python, so we can consider to redefine a function timeit, pass the foo references to him, and then in the timeit calling foo and timing, so that we can't reached change foo defines the purpose of, and, no matter how much B looked at the function, we don't have to go to modify the function definition!

import time
 
def foo():
    print 'in foo()'
 
def timeit(func):
    start = time.clock()
    func()
    end =time.clock()
    print 'used:', end - start
 
timeit(foo)

There seems to be no problem logically, everything is fine and working well! ... Wait, we seem to have changed the code that calls the part. Instead of calling foo(), we changed it to timeit(foo). So, if foo gets called at N, you have to change the code at N. Or even more extreme, consider that the code that is called in one of these cases cannot be modified, for example: this is a function that you give to someone else to use.

1.3. Minimize changes!

In that case, let's figure out a way to not change the calling code; If you don't change the calling code, that means calling foo() needs to have the same effect as calling timeit(foo). We can think of assigning timeit to foo, but timeit seems to take an argument... Try to unify the parameters! If timeit(foo) instead of producing the call directly returns a function that matches the foo argument list... It's easy to assign the return value of timeit(foo) to foo, and then the code that calls foo() doesn't have to change at all!

#-*- coding: UTF-8 -*-
import time
 
def foo():
    print 'in foo()'
 
# Define a timer, pass in one, and return another method with a timer attached
def timeit(func):
    
    # Defines an embedded wrapper function that wraps the incoming function with the timing function
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    
    # Returns the wrapped function
    return wrapper
 
foo = timeit(foo)
foo()

In this way, a simple timer is ready! All we have to do is add foo = timeit(foo) before we call foo after we define foo, and that's the idea of a decorator, so it looks like foo was decorated by timeit. In this example, the timing of function entry and exit is called an Aspect, and this type of Programming is called aspect-oriented Programming. In contrast to the traditional programming practice of executing from the top down, it is like inserting a piece of logic horizontally into the flow of function execution. You can reduce a lot of duplicate code in a particular business area. There are also quite a few terms for section oriented programming, so I won't introduce them here. If you are interested, you can look for relevant information.

This example is for demonstration purposes only and does not consider foo with arguments and return values. The job of perfecting it is up to you :)

The above code looks like it can't be simplified any more, so Python provides a syntax sugar to reduce character input.

import time
 
def timeit(func):
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()

Focus on @timeit on line 11. Adding this line to the definition is the exact same thing as writing foo = timeit(foo). In addition to having less character input, there's an added benefit: it looks more like a decorator.

To understand python decorators, we must first understand that functions are also considered objects in python. This is important. Here's an example:

def shout(word="yes") :
    return word.capitalize()+" !"
 
print shout()
# The output : 'Yes !'
 
# As an object, you can assign functions to any other object variable
 
scream = shout
 
# Notice that we're not using parentheses, because we're not calling a function
# Let's take the delta function shout Assigned to scream That means you can pass scream call shout
 
print scream()
# The output : 'Yes !'
 
# Also, you can delete old names shout But you can still pass scream To access the function
 
del shout
try :
    print shout()
except NameError, e :
    print e
    # The output : "name 'shout' is not defined"
 
print scream()
# The output : 'Yes !'

Aside from that, let's look at another interesting property of python: you can define functions within functions:
def talk() :
 
    # You can be in talk Defines another function in
    def whisper(word="yes") :
        return word.lower()+"...";
 
    # ... And use it right away
 
    print whisper()
 
# Every time you call 'talk' Defined in the talk The inside of the whisper It's also called
talk()
# The output :
# yes...
 
# but "whisper" They don't exist alone :
 
try :
    print whisper()
except NameError, e :
    print e
    # The output : "name 'whisper' is not defined"*

Function reference

From the above two examples, we can conclude that since the function is an object, therefore:

1. It can be assigned to other variables

2. It can be defined in another function

This means that a function can return a function. See the following example:

def getTalk(type="shout") :
 
    # Let's define another function
    def shout(word="yes") :
        return word.capitalize()+" !"
 
    def whisper(word="yes") :
        return word.lower()+"...";
 
    # And then we return one of them
    if type == "shout" :
        # We didn't use (), Because we're not calling this function
        # We're returning the function
        return shout
    else :
        return whisper
 
# And then how do you use it ?
 
# Assign the function to a variable
talk = getTalk()    
 
# Here you can see it talk It's actually a function object :
print talk
# The output : <function shout at 0xb7ea817c>
 
# This object is one of the objects returned by the function :
print talk()
 
# Or you can call it directly as follows :
print getTalk("whisper")()
# The output : yes...

Also, since we can return a function, we can pass it as an argument to the function:
def doSomethingBefore(func) :
    print "I do something before then I call the function you gave me"
    print func()
 
doSomethingBefore(scream)
# The output :
#I do something before then I call the function you gave me
#Yes !

Here you have enough to understand decorators, others can be considered wrappers. That is, it allows you to execute code before and after decoration without changing the contents of the function itself.

decorative

So how do you do manual decoration?

#  An decorator is a function whose argument is another function 
def my_shiny_new_decorator(a_function_to_decorate) :
 
    # Internally, another function is defined: a wrapper.
    # This function encapsulates the original function, so you can execute some code before or after it
    def the_wrapper_around_the_original_function() :
 
        # Put some code that you want before the actual function executes
        print "Before the function runs"
 
        # Execute the original function
        a_function_to_decorate()
 
        # Put some code that you want after the original function executes
        print "After the function runs"
 
    # At the moment, "a_function_to_decrorate" Not yet executed, we return the wrapper function we created
    # The wrapper contains the function and the code it executes before and after, and it is ready
    return the_wrapper_around_the_original_function
 
# Now imagine that you've created a function that you'll never touch again
def a_stand_alone_function() :
    print "I am a stand alone function, don't you dare modify me"
 
a_stand_alone_function()
# The output : I am a stand alone function, don't you dare modify me
 
# Well, you can encapsulate it to implement an extension of behavior. You can simply throw it in the decorator
# The decorator will dynamically encapsulate it with the code you want and return a new available function.
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
# The output :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

Now you may require when each invocation a_stand_alone_function, actual call a_stand_alone_function_decorated. The implementation is also simple, with my_shiny_new_decorator to reassign a_stand_alone_function.
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
# The output :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
 
# And guess what, that's EXACTLY what decorators do !

Decorator revealed

For the previous example, we can use the decorator syntax:

@my_shiny_new_decorator
def another_stand_alone_function() :
    print "Leave me alone"
 
another_stand_alone_function()
# The output :
#Before the function runs
#Leave me alone
#After the function runs

Of course you can also accumulate decorations:
def bread(func) :
    def wrapper() :
        print "</''''''>"
        func()
        print "<______/>"
    return wrapper
 
def ingredients(func) :
    def wrapper() :
        print "#tomatoes#"
        func()
        print "~salad~"
    return wrapper
 
def sandwich(food="--ham--") :
    print food
 
sandwich()
# The output : --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs :
#</''''''>
# #tomatoes#
# --ham--
# ~salad~
#<______/>

Using the python decorator syntax:
@bread
@ingredients
def sandwich(food="--ham--") :
    print food
 
sandwich()
# The output :
#</''''''>
# #tomatoes#
# --ham--
# ~salad~
#<______/>

The order of decorators is important and should be noted :
@ingredients
@bread
def strange_sandwich(food="--ham--") :
    print food
 
strange_sandwich()
# The output :
##tomatoes#
#</''''''>
# --ham--
#<______/>
# ~salad~

Finally, answer the above question:
#  A decorator makebold Used to convert to bold 
def makebold(fn):
    # The result returns the function
    def wrapper():
        # Insert some code before and after execution
        return "<b>" + fn() + "</b>"
    return wrapper
 
# A decorator makeitalic Used to convert to italics
def makeitalic(fn):
    # The result returns the function
    def wrapper():
        # Insert some code before and after execution
        return "<i>" + fn() + "</i>"
    return wrapper
 
@makebold
@makeitalic
def say():
    return "hello"
 
print say()
# The output : <b><i>hello</i></b>
 
# Is equivalent to
def say():
    return "hello"
say = makebold(makeitalic(say))
 
print say()
# The output : <b><i>hello</i></b>

Built-in decorator

There are three built-in decorators: staticmethod, classmethod, and property, which convert instance methods defined in a class into static methods, class methods, and class properties, respectively. Since functions can be defined in modules, static methods and class methods are not very useful unless you want full object-oriented programming. And attributes are not indispensable, and Java lives well without them. From my personal Python experience, I have never used property and use staticmethod and classmethod very infrequently.

class Rabbit(object):
    
    def __init__(self, name):
        self._name = name
    
    @staticmethod
    def newRabbit(name):
        return Rabbit(name)
    
    @classmethod
    def newRabbit2(cls):
        return Rabbit('')
    
    @property
    def name(self):
        return self._name

The property defined here is a read-only property. If you need to be writable, you need to define another setter:
@name.setter
def name(self, name):
    self._name = name

Functools module

The functools module provides two decorators. This module is a new addition to Python 2.5, and is generally used by more than this version. But my normal working environment is 2.4t-t

2.3.1. Wraps (wrapped [, assigned] [, updated]) :
This is a very useful decorator. Those of you who read reflection in the previous article should know that functions have a few special properties such as the function name. After being decorated, the function name foo in the previous example will become the wrapper for the name of the wrapper function. If you want to use reflection, it may lead to unexpected results. This decorator solves this problem by preserving the special properties of the decorated function.

import time
import functools
 
def timeit(func):
    @functools.wraps(func)
    def wrapper():
        start = time.clock()
        func()
        end =time.clock()
        print 'used:', end - start
    return wrapper
 
@timeit
def foo():
    print 'in foo()'
 
foo()
print foo.__name__

First notice line 5. If you comment this line, foo. s name is 'wrapper'. In addition, I believe you have noticed that this decorator actually has a parameter. In fact, he has two other optional arguments, the attribute names in assigned are replaced by assignments, and the attribute names in updated are merged by update. You can get their default values by looking at the source code for functools. For this decorator, it's wrapper = functools.wraps(func)(wrapper).

2.3.2. Total_ordering (CLS) :
This decorator is useful for certain situations, but it was added after Python 2.7. Its function is to add other comparison methods for classes that implement at least one of these, s/s, s/s, s/s, s/s. This is a class decorator. If you feel difficult to understand, might as well take a closer look at the decorator source code:

def total_ordering(cls):
      """Class decorator that fills in missing ordering methods"""
      convert = {
          '__lt__': [('__gt__', lambda self, other: other < self),
                     ('__le__', lambda self, other: not other < self),
                     ('__ge__', lambda self, other: not self < other)],
          '__le__': [('__ge__', lambda self, other: other <= self),
                     ('__lt__', lambda self, other: not other <= self),
                     ('__gt__', lambda self, other: not self <= other)],
          '__gt__': [('__lt__', lambda self, other: other > self),
                     ('__ge__', lambda self, other: not other > self),
                     ('__le__', lambda self, other: not self > other)],
          '__ge__': [('__le__', lambda self, other: other >= self),
                     ('__gt__', lambda self, other: not other >= self),
                     ('__lt__', lambda self, other: not self >= other)]
      }
      roots = set(dir(cls)) & set(convert)
      if not roots:
          raise ValueError('must define at least one ordering operation: < > <= >=')
      root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__
      for opname, opfunc in convert[root]:
          if opname not in roots:
              opfunc.__name__ = opname
              opfunc.__doc__ = getattr(int, opname).__doc__
              setattr(cls, opname, opfunc)
      return cls

I hope this article has helped you with your Python programming.


Related articles: