SoFunction
Updated on 2025-03-01

A deep understanding of Python decorator

Before talking about Python decorators, I would like to give an example first. Although it is a bit dirty, it is very appropriate to the topic of decorators.

The main function of everyone's underwear is to cover up shame, but in winter it cannot protect us from wind and cold. What should we do? One way we thought of was to transform the underwear to make it thicker and longer. In this way, it not only has a shame protection function, but also provides warmth. However, there is a problem. After we transformed this underwear into trousers, although it also has a shame protection function, it is essentially no longer a real underwear. So smart people invented trousers and put them on the outside of the underwear without affecting the underwear. In this way, the underwear is still underwear. After the trousers are trousers, the baby will no longer be cold. The decorators are like the trousers we talk about here. They provide our body with warmth without affecting the effect of the underwear.

Before talking about decorators, you must first understand one thing. Functions in Python are different from Java and C++. Functions in Python can be passed as parameters to another function like ordinary variables, such as:

def foo():
  print("foo")

def bar(func):
  func()

bar(foo)

Officially back to our subject. A decorator is essentially a Python function or class. It allows other functions or classes to add additional functions without any code modification. The return value of the decorator is also a function/class object. It is often used in scenarios with sectional needs, such as: insertion logs, performance testing, transaction processing, caching, permission verification and other scenarios. Decorators are an excellent design to solve this type of problem. With the decorator, we can extract a large amount of similar code that is not related to the function function itself into the decorator and continue to reuse it. In summary, the function of the decorator is to add additional functions to existing objects.

Let's take a look at a simple example, although the actual code may be much more complicated than this:

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

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

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

If the functions bar() and bar2() also have similar requirements, how to do it? Write another logging in the bar function? This creates a large amount of similar code. In order to reduce duplicate code writing, we can do this and redefine a new function: specifically process the log, and then execute the real business code after the log is processed.

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

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

use_logging(foo)

This is logically fine, and the function is implemented, but when we call it, we no longer call the real business logic foo function, but instead use_logging function, which destroys the original code structure. Now we have to pass the original foo function as a parameter to the use_logging function every time. So is there a better way? Of course there is, the answer is the decorator.

Simple decorator

def use_logging(func):

def wrapper():
    ("%s is running" % func.__name__)
return func()  # When passing foo as a parameter, executing func() is equivalent to executing foo()return wrapper

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

foo = use_logging(foo) # Because the function object wrapper returned by the decorator use_logging(foo) , this statement is equivalent to foo = wrapperfoo()          # Execution of foo() is equivalent to executing wrapper()

use_logging is a decorator, it is an ordinary function, which wraps the function func that executes the real business logic, and it looks like foo is decorated with use_logging. The return of use_logging is also a function, and the name of this function is wrapper. In this example, when a function enters and exits, it is called a cross-sectional plane, and this programming method is called a section-oriented programming.

@ Syntax Sugar

If you have been in Python for a while, 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 that the last step of the assignment can be omitted.

def use_logging(func):

def wrapper():
    ("%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 the sentence foo = use_logging(foo) and call foo() directly to get the desired result. Have you seen that the foo() function does not need to be modified, just add a decorator to the definition, and the call is still 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 convenience of using decorators in Python is attributed to the fact that Python functions can be passed as parameters to other functions 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):
    ("%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 asked again, what if the foo function receives two parameters? What about the three parameters? What's more, I may have passed on many. When the decorator does not know how many parameters foo has, we can use *args instead:

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

In this way, no matter how many parameters foo are defined, I can pass them into func in full. This will not affect foo's business logic. At this time, readers will ask, what if the 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))

At this time, you can specify the wrapper function to specify the keyword function:

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

Decorators with parameters

Decorators also have greater flexibility, such as decorators with parameters. In the above decorators call, the only parameter that the decorators receive is the function foo that executes the business. The syntax of the decorator allows us to provide other parameters when calling, such as @decorator(a). This provides greater flexibility for the writing and use of decorators. For example, we can specify the level of the log in the decorator, because different business functions may require different log levels.

def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
        ("%s is running" % func.__name__)
elif level == "info":
        ("%s is running" % func.__name__)
return func(*args)
return wrapper

return decorator

@use_logging(level="warn")
def foo(name='foo'):
  print("i am %s" % name)

foo()

The above use_logging is a decorator that allows parameters. It is actually a function encapsulation of the original decorator and returns a decorator. We can understand it as a closure containing parameters. When we call it using @use_logging(level=”warn”), Python can discover this layer of encapsulation and pass the parameters to the decorator environment.

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

Class decorator

That's right, a decorator can not only be a function, but also a class. Compared with a function decorator, a class decorator has the advantages of high flexibility, high cohesion, and packaging. Using class decorators mainly relies on the class's __call__ method. This method is called when the decorator is attached to a function using the @ form.

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

def __call__(self):
print ('class decorator runing')
    self._func()
print ('class decorator ending')

@Foo
def bar():
print ('bar')

bar()

Using a decorator greatly reuses the code, but one of its disadvantages is that the meta information of the original function is gone, such as the docstring, __name__, and parameter list of the function. Let's take a look at the example first:

# Decoratorsdef logged(func):
def with_logging(*args, **kwargs):
print func.__name__   # Output 'with_logging'print func.__doc__    # Output Nonereturn 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. Of course, its docstring, __name__ becomes the information of the with_logging function. Fortunately, we have it. wraps itself is also a decorator. It can copy the meta information of the original function into the func function in the decorator, which makes the func function in the decorator also have the same meta information as the original function foo.

from functools import wraps
def logged(func):
  @wraps(func)
def with_logging(*args, **kwargs):
print func.__name__   # Output 'f'print func.__doc__    # output 'does some math'return func(*args, **kwargs)
return with_logging

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

Decorator order

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

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

Its execution order is from the inside out, first calling the innermost decorator, and finally calling the outermost decorator, which is equivalent to

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

Thank you for reading, I hope it can help you. Thank you for your support for this site!