Python function decorator

Basic knowledge

Everything is an object

First, let's understand the functions in Python:

def hi(name="yasoob"):
    return "hi " + name
 
print(hi())
# output: 'hi yasoob'
 
# We can even assign a function to a variable, such as
greet = hi
# We don't use parentheses here because we're not calling the hi function
# Instead, you're putting it in the green variable. Let's try to run this
 
print(greet())
# output: 'hi yasoob'
 
# If we delete the old hi function, see what happens!
del hi
print(hi())
#outputs: NameError
 
print(greet())
#outputs: 'hi yasoob'

Define functions in functions

Those were the basics of functions just now. Let's take your knowledge further. In Python, we can define another function in one function:

def hi(name="yasoob"):
    print("now you are inside the hi() function")
 
    def greet():
        return "now you are in the greet() function"
 
    def welcome():
        return "now you are in the welcome() function"
 
    print(greet())
    print(welcome())
    print("now you are back in the hi() function")
 
hi()
#output:now you are inside the hi() function
#       now you are in the greet() function
#       now you are in the welcome() function
#       now you are back in the hi() function
 
# It shows that whenever you call hi(), greet() and welcome() will be called at the same time.
# Then, the greet() and welcome() functions cannot be accessed outside the hi() function, for example:
 
greet()
#outputs: NameError: name 'greet' is not defined

Now we know that we can define another function in the function. That is: we can create nested functions. Now you need to learn a little more, that is, functions can also return functions.

Returns a function object from a function

In fact, it is not necessary to execute another function in one function. We can also return it as output:

def hi(name="yasoob"):
    def greet():
        return "now you are in the greet() function"
 
    def welcome():
        return "now you are in the welcome() function"
 
    if name == "yasoob":
        return greet
    else:
        return welcome
 
a = hi()
print(a)
#outputs: <function greet at 0x7f2143c01500>
 
#The above clearly shows that ` a 'now points to the greet() function in the hi() function
#Now try this
 
print(a())
#outputs: now you are in the greet() function

Look at this code again. In the if/else statement, we return greet and welcome instead of greet() and welcome(). Why is that? This is because when you put a pair of parentheses after it, the function will execute; However, if you don't put parentheses after it, it can be passed everywhere and assigned to other variables without executing it. Do you understand Let me explain in a little more detail.

When we write a = hi(), hi() will be executed, and since the name parameter is yasoob by default, the function greet is returned. If we change the statement to a = hi(name = "ali"), the welcome function will be returned. We can also print out hi()(), which will output now you are in the greet() function.

Pass a function as an argument to another function

def hi():
    return "hi yasoob!"
 
def doSomethingBeforeHi(func):
    print("I am doing some boring work before executing hi()")
    print(func())
 
doSomethingBeforeHi(hi)
#outputs:I am doing some boring work before executing hi()
#        hi yasoob!

Now you have all the necessary knowledge to further learn what a decorator really is. Decorators let you execute code before and after a function.

What is a decorator

Decorators are essentially a Python function or class, which can add additional functions to other functions or classes without any code modification. The return value of decorators is also a function / class object. It is often used in scenarios with various requirements, such as log insertion, performance testing, transaction processing, caching, permission verification, etc. the decorator is an excellent design to solve such problems. With the decorator, we can extract a large number of similar code independent of the function itself into the decorator and continue to reuse it. Generally speaking, the function of decorators is to add additional functions to existing objects. Let's start with a simple example, although the actual code may be much more complex than this:

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

Now there is a new requirement to record the function execution log, so add the log code to the code:

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

What if the functions bar() and bar2() have similar requirements? Write another logging in the bar function? This will result in a large number of identical code. In order to reduce repeated code writing, we can redefine a new function: specifically handle logs, and execute real business code after log processing

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

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

use_logging(foo)

There is no logical problem in this way. The function is realized, but we no longer call the real business logic foo function, but use instead_ Logging function, which destroys the original code structure. Now we have to pass the original foo function as a parameter to use every time_ Logging function, 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()   # When foo is passed in as a parameter, executing func() is equivalent to executing foo()
    return wrapper

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

foo = use_logging(foo)  # Because the decorator_ The function object wrapper returned by logging (foo) is equivalent to foo = wrapper
foo()                   # Executing foo() is equivalent to executing wrapper()

use_logging is a decorator. It is an ordinary function. It wraps the function func that executes real business logic. It looks like foo is used_ Logging decorated the same, use_logging also returns a function called wrapper. In this example, when the function enters and exits, it is called a cross section. This programming method is called section oriented programming.

@Grammar sugar

If you have been in contact with Python for some time, you must be familiar with the @ symbol. Yes, the @ symbol is the syntax sugar of the decorator. It is placed at the beginning of the function definition, so 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 omit foo = use_logging(foo). Call foo() directly to get the desired result. Did you see that the foo() function does not need to be modified, just add a decorator at the defined place, and the call is the same as before. If we have other similar functions, we can continue to call the decorator to modify the function without repeatedly modifying the function or adding new encapsulation. In this way, we improve the reusability of the program and increase the readability of the program.

The reason why decorators are so convenient in Python is that Python functions can be passed to other functions as parameters like ordinary objects, can be assigned to other variables, can be used as return values, and can be defined in another function.

*args,**kwargs

Someone may ask, what if my business logic function foo needs parameters? For example:

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

In this way, the parameters defined by the foo function can be defined in the wrapper function. At this time, someone has to ask, what if the foo function receives two parameters? What about the three parameters? What's more, I may pass many. When the decorator does not know 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

In this way, no matter how many parameters foo defines, I can completely pass them to func. This will not affect the business logic of foo. At this time, some readers will ask, what if foo function also defines some keyword parameters? For example:

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

In this case, you can specify the wrapper function as a keyword function:

def wrapper(*args, **kwargs):
        # args is an array and kwargs is a dictionary
        logging.warn("%s is running" % func.__name__)
        return func(*args, **kwargs)
    return wrapper

Decorator sequence

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

@a
@b
@c
def f ():
    pass

Its execution sequence is from inside to outside, it calls the most interior decorator first, finally calls the outermost decorator, it is equivalent to

f = a(b(c(f)))

Keywords: Python

Added by Hannes2k on Wed, 06 Oct 2021 17:29:03 +0300