Complete introduction and practice of Python Decorator

Have you ever encountered this scenario: after working hard to write a bunch of core business functions, you suddenly want to add an execution log to each function; or you find that several key functions need to record the **execution time ** in batches. If you directly copy and paste the repeated code at the beginning and end of the function, it will not only be cumbersome, but also require hundreds of changes during future maintenance - at this time, Python Decorator will become your savior!

Decorators are a very "Pythonic" syntax feature in Python that allow you to elegantly insert additional functionality without modifying the internal code of the original function. This article will help you start from scratch and thoroughly understand the principles and usage of decorators.

1. Decorator basics: two prerequisite knowledge

A decorator is essentially a higher-order function. To understand it, you must first understand two core concepts.

1.1 Functions are also objects

In Python, functions are “first-class citizens” just like integers, strings, and lists:

  • Can be assigned to variables
  • Can be used as a parameter of another function
  • Can be used as the return value of another function
  • Can be stored in lists, dictionaries and other containers

Let’s first look at an example of assignment:

def get_current_date():
    print('2024-06-01')

f = get_current_date   # 注意:不带括号!是把函数对象本身赋给变量 f
f()                    # 带括号才是调用函数
# 输出: 2024-06-01

# 函数对象都有元属性,比如 __name__ 显示函数名
print(get_current_date.__name__)  # get_current_date
print(f.__name__)                 # 同样是 get_current_date,f 只是别名

This code illustrates that the function itself is an object that can be passed and referenced, which paves the way for decorators.

1.2 Higher-order functions

The so-called higher-order function is a function that accepts a function as a parameter or returns a function as a return value.

Below we write a simple higher-order function that receives a function and adds printing before and after its execution:

def prepare_exec(func):
    print("=== 准备执行函数 ===")
    func()               # 调用传入的函数
    print("=== 函数执行完毕 ===")

def say_hello():
    print("Hello Python Deco!")

prepare_exec(say_hello)
# 输出:
# === 准备执行函数 ===
# Hello Python Deco!
# === 函数执行完毕 ===

Although this works, every time you callsay_helloYou have to wrap it manuallyprepare_exec, too much trouble. can you letprepare_execDirectly "transform"say_hello, call it directly latersay_hello()Can it automatically bring forward and backward printing?

This is the prototype idea of a decorator: return a new wrapped function and replace the original function with it.

2. The first decorator: from manual to automatic

2.1 Basic decorator + @ syntax sugar

Let's put the aboveprepare_execChange to return a "wrapper function"wrapper,thiswrapperInternally, additional logic is executed before calling the original function:

def log(func):
    def wrapper(*args, **kwargs):   # *args, **kwargs 接受任意参数
        print(f'调用 {func.__name__}() 函数')
        return func(*args, **kwargs)  # 注意必须返回原函数的执行结果!
    return wrapper

Now, use the decorator syntactic sugar provided by Python@**, paste it above the function that needs to be enhanced:

@log
def get_current_date():
    print('2024-06-01')

@log
def add(a, b):
    return a + b

Called exactly as is:

get_current_date()
# 输出:
# 调用 get_current_date() 函数
# 2024-06-01

result = add(1, 2)
print(result)
# 输出:
# 调用 add() 函数
# 3

The magic behind:@logPlacing it before the function definition is equivalent to the Python interpreter automatically executing:

get_current_date = log(get_current_date)

In other words, the original function is replaced by the new wrapped function.

2.2 Retain meta-information of the original function

There is a small problem with the above decorator: the meta-properties of the function have changed after wrapping!

print(get_current_date.__name__)  # 输出: wrapper,而不是 get_current_date

This is unfriendly for debugging, documentation generation, and some tools that rely on meta-properties. The fix is ​​simple, with the help of the Python standard libraryfunctools.wraps

import functools

def log(func):
    @functools.wraps(func)      # 自动将 func 的 __name__、__doc__ 等复制给 wrapper
    def wrapper(*args, **kwargs):
        print(f'调用 {func.__name__}() 函数')
        return func(*args, **kwargs)
    return wrapper

Check again:

print(get_current_date.__name__)  # 输出: get_current_date

Perfect! When writing decorators in the future, remember to develop a@functools.wrapshabits.

3. Advanced: Let the decorator accept custom parameters

In a real project, you may need the decorator to support different configurations. For example, the log decorator sometimes uses[INFO]prefix, sometimes used[DEBUG]. This requires the decorator to receive its own parameters.

The implementation method is to add a layer of functions: the outermost layer receives custom parameters, the middle layer is the original decorator, and the innermost layer is the wrapper function.

import functools

def log(prefix="[INFO]"):   # 最外层:接收自定义参数
    def decorator(func):    # 中间层:原来的装饰器(返回 wrapper 的高阶函数)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'{prefix} 调用 {func.__name__}()')
            return func(*args, **kwargs)
        return wrapper
    return decorator

How to use:

# 指定前缀
@log(prefix="[DEBUG]")
def add(a, b):
    return a + b

# 使用默认前缀(此时必须带括号!)
@log()
def get_current_date():
    print('2024-06-01')

Execution Principle:

  1. Execute firstlog(prefix="[DEBUG]"), get the return valuedecorator
  2. Execute again@decorator, equivalent toadd = decorator(add)

If you think the default parameters have to be written@log()It's a bit troublesome. Section 4.2 below will give a general solution that can be used with or without parentheses.

4. Practice: Two high-frequency decorators

After finishing the theory, let’s write two decorators that are used in actual projects.

4.1 Calculate function execution time (performance analysis tool)

When doing performance optimization, you often need to know how long a certain function took to run. Note: Use heretime.perf_counter()instead oftime.time(), because the former is a short-time timer with high precision and not affected by system time adjustment.

import time
import functools

def metric(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f'{func.__name__} 执行耗时: {(end - start) * 1000:.2f} ms')
        return result
    return wrapper

@metric
def fast_add(x, y):
    time.sleep(0.0012)
    return x + y

@metric
def slow_multiply(x, y, z):
    time.sleep(0.1234)
    return x * y * z

4.2 Universal log decorator (supported by both calling methods)

If you want to make the log decorator more flexible - support both@logWithout parentheses, it is also supported@log("前缀")With parameters, you can design it like this:

import functools

def log(text_or_func=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if callable(text_or_func):
                # 不带括号:text_or_func 实际是原函数本身
                print(f'调用 {func.__name__}()')
            else:
                # 带括号:text_or_func 是自定义前缀
                print(f'{text_or_func} 调用 {func.__name__}()')
            return func(*args, **kwargs)
        return wrapper

    # 关键判断:如果第一个参数是函数,则直接进入装饰流程
    if callable(text_or_func):
        func = text_or_func
        text_or_func = None
        return decorator(func)
    else:
        return decorator

Test it out:

@log
def func1():
    pass

@log("[DEBUG]")
def func2():
    pass

Both writing methods are perfectly compatible!

5. Advanced: Use classes to implement decorators

In addition to functions, we can also use classes to implement decorators. Class decorators have two outstanding advantages:

  1. Convenient to save state (such as counting how many times a function has been called)
  2. Functions can be extended through inheritance

5.1 Simple class decorator

Class decorators need to be in__init__Receive the original function and retain the information in__call__Implement packaging logic in . Since there is no@functools.wrapsCan be applied directly, needs to be called manuallyfunctools.update_wrapperto preserve meta attributes.

import functools

class Logger:
    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)  # 将 func 的元信息复制到实例上

    def __call__(self, *args, **kwargs):
        print(f'调用 {self.func.__name__}()')
        return self.func(*args, **kwargs)

@Logger
def get_current_date():
    print('2024-06-01')

5.2 Stateful class decorator - counting the number of calls

This is a classic use case for class decorators: using instance properties to save a count.

import functools

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} 已被调用 {self.count} 次")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
# 输出:
# say_hello 已被调用 1 次
# Hello!
# say_hello 已被调用 2 次
# Hello!

every callsay_hello(), the counter automatically accumulates and the status is perfectly retained.

6. Decorator best practices

  1. Retain original function meta-information: Always remember to usefunctools.wraps(function decorator) orfunctools.update_wrapper(class decorator).
  2. Single Responsibility: A decorator only does one thing. Don't do timing, logging, and permission checks at the same time. It's easier to reuse after splitting.
  3. Maintain configurability: Try to let the decorator accept optional parameters to adapt to different scenarios, just like the previous generallog
  4. Avoid excessive nesting: Decorators will add a layer of abstraction. If three or four layers are stacked together, the code execution flow will become obscure and debugging will be more difficult. If it can be solved in a simple way, don't show off your skills.

7. Summary

Now looking back at the requirements of "batch adding logs and batch timing" at the beginning, do you feel that it is much easier? Write a useful decorator and add it to the header of the required function.@That's fine, the core business code is not affected at all.

The core value of decorators is:

  • Non-intrusive extension: without modifying a single line of code of the original function
  • Crosscutting concern processing: logging, timing, permission checking, Python’s own@functools.lru_cacheCaching, etc., these are functions that have nothing to do with the core logic but are everywhere
  • Code is cleaner and more Pythonic

Mastering decorators will definitely improve the quality of your Python code. Try adding the first decorator to a function in your project now!