Python 深入探索装饰器(语法糖)



文章开篇

Python的魅力,犹如星河璀璨,无尽无边;人生苦短、我用Python!


装饰器的概念

装饰器是Python的一个高级功能,又称语法糖
它允许我们在不修改函数或方法源代码的情况下,给它们增加额外的功能
装饰器本质上是一个接受函数作为参数的高阶函数,并返回一个新的函数
这个新的函数通常会包含原始函数的功能,并可能添加一些额外的功能,如日志记录、性能测试、权限验证等;


装饰器的原理

装饰器的原理是基于闭包和函数作为一等对象的特性
当一个函数被装饰时,实际上是将被装饰的函数作为参数传递给装饰器函数
然后在装饰器函数的内部定义一个新的函数,该新函数包含了额外的功能,并调用了被装饰的函数
最后,装饰器返回这个新函数,从而实现了对被装饰函数的行为扩展;


装饰器的工作流程

  • 定义一个装饰器函数,该函数接受一个函数作为参数;
  • 在装饰器函数内部,可以添加一些额外的逻辑或功能
  • 装饰器函数返回一个新的函数,这个新函数通常会调用被装饰的函数,并可能在其前后添加额外的逻辑;
  • 使用**@**符号将装饰器函数应用到需要被装饰的函数上;
  • 当被装饰的函数被调用时,实际上是调用了装饰器返回的新函数,这个新函数会先执行装饰器中的额外逻辑,然后再调用原始函数

为什么使用装饰器

  • **代码复用:**可以将通用的功能封装在装饰器中,避免重复编写代码;
  • **代码简洁:**可以将函数的核心逻辑与额外功能分离,使代码更易读、易维护;
  • **动态扩展:**可以在不修改原始函数代码的情况下,动态地添加新功能;

什么时候需要用装饰器

  • 需要在多个函数之间共享某些逻辑时,如日志记录、性能测试、缓存等;
  • 需要动态修改函数行为时,如权限验证、事务管理等;
  • 需要为函数添加额外的注解或元数据时,如函数的文档字符串、版本信息等;

函数装饰器


1.不带参数的函数装饰器

假设,希望希望获取一个函数的执行开始时间、结束时间、执行耗时,我们可以使用装饰性;
当然,不使用装饰器也是完全可以实现的,也就是说装饰器并不是编码必须性
但是,如果有很多类似的函数需要添加相同的功能,或者后续需求变化需要添加日志功能,那么修改每个函数的代码将变得非常繁琐和冗余;

import random
import time


# 计算函数执行耗时的装饰器
def timing(func):
    # 如果明确传递的参数是什么可以直接写形参,如被装饰的函数只需要一个count参数,那么就写count即可
    # def wrapper(count):
    # 如果不确定或暂时未定义参数建议使用不定长形式,以便被装饰的函数后续扩展添加参数
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print("函数:", func.__name__, "开始时间:", start_time)
        # result = func(count)
        result = func(*args, **kwargs)
        end_time = time.time()
        print("函数:", func.__name__, "结束实际:", end_time)
        print("函数:", func.__name__, "执行耗时:", end_time - start_time)
        return result

    return wrapper


@timing
def task(count: int) -> list:
    """
    模拟一个耗时任务,循环指定次数,每次等待1秒并产生一个0-9的随机数,最后返回产生的随机数列表
    :param count: 循环次数
    :return: list
    """
    random_numbers = []
    if not isinstance(count, int):
        raise ValueError("参数无效")
    for i in range(count):
        time.sleep(1)
        random_numbers.append(random.randint(0, 9))
    return random_numbers


if __name__ == '__main__':
    print(task(10))
    # 函数: task 开始时间: 1709276546.2919512
    # 函数: task 结束实际: 1709276556.320051
    # 函数: task 执行耗时: 10.028099775314331
    # [5, 8, 7, 2, 7, 3, 6, 4, 5, 7]

2.带参数的函数装饰器

通过上述详细的概念、原理和工作流程的讲解,你应该能够对装饰器的作用有一定的认识了;
不过装饰器的用法还远不止如此,深究下去,还大有文章;
上面普通装饰器的示例,是不能接受参数的,只能适用于一些简单的场景
装饰器本身就是一个函数,作为一个函数,不接受传参,那么这个函数的功能就会大打折扣;
如用作校验用户登录时检查账号密码,那么被装饰的函数可以只关注具体的业务逻辑;


def login_check(username, password):
    account = {"admin": "admin", "root": "root", "guest": "guest"}

    def wrapper(func):
        def inside(*args, **kwargs):
            account_flag = account.get(username)
            if account_flag is None or account_flag != password:
                return "登录失败"

            # 真正执行函数的地方,如果上述条件不成立,则跳过调用被装饰的函数
            return func(*args, **kwargs)

        return inside

    return wrapper


# 小明,输入正确的账号和密码
@login_check(username="admin", password="admin")
def xiaoming():
    return "登录成功"


# 张三,输入错误的账号和密码
@login_check(username="root", password="10086")
def zhangsan():
    return "登录成功"


if __name__ == '__main__':
    # 装饰器中验证通过,执行被装饰的函数
    print(xiaoming())   # 登录成功

    # 装饰器中验证失败,装饰器中断,未执行被装饰的函数
    print(zhangsan())   # 登录失败

类装饰器

解释类装饰器前,需要引入两个魔法方法的概念;
_init_()

  • 类的初始化构造方法,它在类发生实例化时自动调用,并要求实例化时需要传递的参数;

_call_()

  • 允许类的实例对象能够像函数一样被调用,当尝试调用一个对象(即使用圆括号)时自动调用该方法;

类装饰器是利用类的特性来增强或修改函数的行为;
定义类装饰器必须创建一个实现了__init__和__call__这两个内置方法的类
__init__方法,用于接收并存储被装饰的函数以及参数,以便在__call__中访问和扩展其功能;

__call__方法,用于被装饰的函数发生调用时自动执行,它是类装饰器的核心


1.不带参数的类装饰器
class Logger:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # 在函数调用前执行的代码
        print(f"日志记录:函数名称: {self.func.__name__}, 执行开始")
        print(f"日志记录:函数位置参数: {args}")
        print(f"日志记录:函数关键字参数: {kwargs}")

        # 调用原始函数并获取结果
        result = self.func(*args, **kwargs)

        # 在函数调用后执行的代码
        print(f"日志记录:函数名称: {self.func.__name__}, 执行结束")

        # 返回函数调用的结果
        return result


# 使用装饰器
@Logger
def hello(name1, name2, name3):
    return f"Hello, {name1, name2, name3}!"


# 调用被装饰的函数
print(hello("张三", name2="李四", name3="王五"))

# 控制台信息如下:
# 日志记录:函数名称: hello, 执行开始
# 日志记录:函数位置参数: ('张三',)
# 日志记录:函数关键字参数: {'name2': '李四', 'name3': '王五'}
# 日志记录:函数名称: hello, 执行结束
# Hello, ('张三', '李四', '王五')!

2.带参数的类装饰器
import faker


class Generate:

    def __init__(self, name, phone, id_card, count=1):
        self.count = count
        self.name = name
        self.phone = phone
        self.id_card = id_card

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            # 在函数调用前执行的代码
            generates = []
            fake = faker.Faker(locale="zh_CN")
            for i in range(self.count):
                data = {}
                if self.name in [True, 1, "1"]:
                    data["姓名"] = fake.name()
                if self.phone in [True, 1, "1"]:
                    data["手机号码"] = fake.phone_number()
                if self.id_card in [True, 1, "1"]:
                    data["身份证号码"] = fake.ssn(min_age=18, max_age=60)
                generates.append(data)
            # 调用原始函数并返回函数调用的结果
            return func(generates=generates, *args, **kwargs)

        return wrapper


# 使用装饰器
@Generate(count=5, name=1, phone=True, id_card="1")
def person(generates: list):
    return generates


# 调用被装饰的函数
for i in person():
    print(i)
    # {'姓名': '陈凤英', '手机号码': '13878201678', '身份证号码': '350305198408251802'}
    # {'姓名': '张欢', '手机号码': '18860615198', '身份证号码': '610302200311160124'}
    # {'姓名': '罗霞', '手机号码': '18812922545', '身份证号码': '360428197501148217'}
    # {'姓名': '黄小红', '手机号码': '14547824221', '身份证号码': '350802197111190846'}
    # {'姓名': '容玉兰', '手机号码': '14522343903', '身份证号码': '110114196904052214'}

装饰器链

概念:

  • 装饰器链是Python中将多个装饰器按顺序应用于函数的方法
  • 函数首先被最内层的装饰器处理,然后依次向外层装饰器执行,最终执行原始函数;
  • 每个装饰器可以对函数进行不同的处理,以实现不同的功能;

优先级:

  • 装饰器链的执行顺序是从内到外,即先执行最内层的装饰器,然后依次向外层装饰器执行,这与递归的调用过程相似。
  • 然而,装饰器链并不是后进先出,而是先自底向上执行装饰器的定义(入栈),然后自顶向下执行装饰器的功能(出栈);


def decorator1(func):
    print("进入装饰器 1")

    def wrapper(number, *args, **kwargs):
        print("--- 执行装饰器 3")
        number = number + 1
        func(number, *args, **kwargs)

    return wrapper


def decorator2(func):
    print("进入装饰器 2")

    def wrapper(number, *args, **kwargs):
        print("--- 执行装饰器 2")
        number = number + 1
        func(number, *args, **kwargs)

    return wrapper


def decorator3(func):
    print("进入装饰器 3")

    def wrapper(number, *args, **kwargs):
        print("--- 执行装饰器 3")
        number = number + 1
        func(number, *args, **kwargs)

    return wrapper


@decorator1 # 第三个执行的装饰器
@decorator2 # 第二个执行的装饰器
@decorator3 # 第一个执行的装饰器(越靠近被装饰函数的装饰器执行优先级越高)
def add_one(number):    # 上述装饰器执行完成后,执行被装饰的函数
    print("最终number的结果:", number)


add_one(1)  # 最终number的结果: 4

拓展-装饰器中@wraps的作用

@functools.wraps(func)是一个装饰器工厂函数,它用于保留原始函数的名称、文档字符串、注解和模块等属性,在创建新的装饰器函数时非常有用;
当你创建一个装饰器时,通常会定义一个函数,该函数接受一个函数作为参数,并返回一个新的函数。
这样做会导致原始函数的某些属性(如 namedoc 等)丢失,因为它们通常会被新函数的属性所覆盖
@functools.wraps(func)的作用就是在返回新函数时,将这些属性从原始函数复制到新函数,以保持原始函数的“外观”。


1.使用@wraps装饰器
import functools


def wraps_decorator(func):
    # 保留原始函数的名称、文档字符串、注解和模块等属性
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@wraps_decorator
def person(name: str, age: int, city: str) -> None:
    """
    模拟用户个人介绍函数
    :param name: 姓名
    :param age:  年龄
    :param city: 城市
    :return: None
    """
    print(f"大家好,我叫{name},今年{age}岁,来自{city},我热爱探索和学习,期待与大家共同进步。")


# 输出函数名和文档字符串
print("函数名称:", person.__name__)    # 函数名称: person
print("函数文档:", person.__doc__)
# 函数文档:
#     模拟用户个人介绍函数
#     :param name: 姓名
#     :param age:  年龄
#     :param city: 城市
#     :return: None
print("函数词典:", person.__dict__)    # {'__wrapped__': <function person at 0x7fd8f01b4af0>}


2.不使用@wraps装饰器

def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@decorator
def person(name: str, age: int, city: str) -> None:
    """
    模拟用户个人介绍函数
    :param name: 姓名
    :param age:  年龄
    :param city: 城市
    :return: None
    """
    print(f"大家好,我叫{name},今年{age}岁,来自{city},我热爱探索和学习,期待与大家共同进步。")


# 输出函数名和文档字符串
print("函数名称:", person.__name__)     # 函数名称: wrapper
print("函数文档:", person.__doc__)      # 函数文档: None
print("函数词典:", person.__dict__)     # 函数词典: {}

在第一个示例中

  • 使用了@functools.wraps(func)装饰器,被装饰函数person保留了原始函数的元数据,包括函数名和文档字符串。

在第二个示例中

  • 没有使用@functools.wraps(func)装饰器被装饰的函数元数据被覆盖为装饰器内部函数的元数据,导致丢失了原始函数的信息。

拓展-装饰器工厂

装饰器工厂的原理是基于判断是否需要在装饰器内部处理某些逻辑并返回一个装饰器函数
装饰器工厂的核心在于它接受一些参数(可能是函数参数、配置选项、环境变量等),然后基于这些参数来决定返回哪个装饰器,这意味着可以动态地创建装饰器,以适应不同的场景和需求


1.参数类型检查装饰器

我们都知道python是动态弱类型语言,类型不显式声明,运行时可根据上下文推断变量或参数类型;
但有时我们定义的函数是给同事或他人使用的,所以类型注解和类型检查变得极为重要;
如下是我真实项目代码中在用的装饰器工厂,用来检查参数的类型

import functools
from inspect import signature


def type_assert(*ty_args, **ty_kwargs):
    """
    一个允许携带参数的装饰器,实现检查被装饰函数的参数类型功能
    :param ty_args:   类型1
    :param ty_kwargs: 类型2
    :return: callable
    """

    # 获取函数参数和它类型之间的映射关系
    def decorator(func):
        # 获取函数参数列表
        sig = signature(func)
        # print("参数列表:", sig)
        # 奖建立类型映射和参数名字,绑定部分检查
        b_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
        # print("类型映射:", b_types)

        # 使用wraps保留被装饰函数的元数据,根据参数列表定制化出来一个装饰器
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            dic = sig.bind(*args, **kwargs).arguments.items()
            # print("被装饰函数的参数元祖:", dic)
            for name, obj in dic:
                # 只有在映射关系中才做类型检查
                if name in b_types:
                    # 检查类型,如果类型不对抛出异常
                    if not isinstance(obj, b_types[name]):
                        raise TypeError(f"参数类型不符,参数{name}必须是{b_types[name]}类型")
            return func(*args, **kwargs)

        return wrapper

    # 返回装饰器
    return decorator


# 使用ty_args形式传递参数
@type_assert(str, int, str)
def func_test_1(name, age, city):
    print(f"大家好,我叫{name},今年{age}岁,来自{city}。")


# 使用ty_kwargs形式传递参数
@type_assert(name=str, age=int, city=str, hobby=list)
def func_test_2(name, age, city, hobby):
    print(f"大家好,我叫{name},今年{age}岁,来自{city},我的爱好是{','.join(hobby)}。")


if __name__ == '__main__':
    func_test_1("张三", 18, "上海") # 大家好,我叫张三,今年18岁,来自上海。
    print(func_test_1.__name__)

    func_test_2("李四", 20, "上海", "打篮球")  # TypeError: 参数类型不符,参数hobby必须是<class 'list'>类型
    print(func_test_2.__name__)

2.自定义日志装饰器

根据log_off参数来决定是否记录日志;
根据record_level参数来决定记录日志的严重级别;
根据log_level参数来决定记录日志的最低严重级别;
根据log_file参数来决定记录日志输出到哪个日志文件;

import functools
import logging


def log_factory(record_level="info", log_level=logging.DEBUG, log_off=True, log_file=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if log_off:
                logger = logging.getLogger(func.__name__)
                logger.setLevel(log_level)
                level = {
                    "info": logger.info,
                    "debug": logger.debug,
                    "warning": logger.warning,
                    "error": logger.error,
                    "critical": logger.critical,
                }
                # 创建格式器
                formatter = logging.Formatter(
                    fmt='%(asctime)s --> %(filename)-15s [line:%(lineno)-3d] --> %(levelname)-5s --> %(message)s')
                # 创建控制台处理器,并应用格式器对象
                console_handler = logging.StreamHandler()
                console_handler.setFormatter(fmt=formatter)
                # 创建文件处理器,并应用格式器对象
                if log_file is None:
                    filename = "test.log"
                else:
                    filename = log_file
                file_handler = logging.FileHandler(filename=filename, mode='a', encoding='utf-8')
                file_handler.setFormatter(fmt=formatter)
                # 添加处理器
                logger.addHandler(console_handler)
                logger.addHandler(file_handler)
                # 根据装饰器传递的参数记录日志等级
                level.get(record_level)(f"日志记录:函数名称:{func.__name__}() 执行开始")
                level.get(record_level)(f"日志记录:函数位置参数:{args}")
                level.get(record_level)(f"日志记录:函数关键字参数:{kwargs}")
                result = func(*args, **kwargs)
                level.get(record_level)(f"日志记录:函数结果:{result}")
                level.get(record_level)(f"日志记录:函数名称:{func.__name__}() 执行结束")
            else:
                result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator


# 使用装饰器工厂创建不同级别的日志装饰器
@log_factory(record_level="info", log_level=logging.DEBUG)
def add(a, b):
    return a + b


@log_factory(record_level="warning", log_level=logging.DEBUG, log_file="test_subtract.log")
def subtract(a, b):
    return a - b


# 调用被装饰后的函数
result1 = add(5, 3)
# 生成指定日志文件名称的日志文件,并按照指定严重级别记录日志,文件内容如下:
# 2024-03-02 17:23:14,698 --> 装饰器工厂2.py       [line:45 ] --> INFO  --> 日志记录:函数名称:add() 执行开始
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:46 ] --> INFO  --> 日志记录:函数位置参数:(5, 3)
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:47 ] --> INFO  --> 日志记录:函数关键字参数:{}
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:49 ] --> INFO  --> 日志记录:函数结果:8
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:50 ] --> INFO  --> 日志记录:函数名称:add() 执行结束

result2 = subtract(10, 4)
# 生成指定日志文件名称的日志文件,并按照指定严重级别记录日志,文件内容如下:
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:45 ] --> WARNING --> 日志记录:函数名称:subtract() 执行开始
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:46 ] --> WARNING --> 日志记录:函数位置参数:(10, 4)
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:47 ] --> WARNING --> 日志记录:函数关键字参数:{}
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:49 ] --> WARNING --> 日志记录:函数结果:6
# 2024-03-02 17:23:14,699 --> 装饰器工厂2.py       [line:50 ] --> WARNING --> 日志记录:函数名称:subtract() 执行结束


常用内置装饰器

1.@staticmethod

@staticmethod是Python中用来定义静态方法的装饰器。静态方法与类方法和实例方法的行为不同,它不需要访问类或实例的属性,因此可以直接通过类来调用。静态方法使用装饰器@staticmethod进行声明。

class MathUtils:
    @staticmethod
    def add(x, y):
        return x + y

# 调用静态方法
result = MathUtils.add(3, 5)
print(result)  # 输出:8

2.@classmethod

@classmethod是Python中用来定义类方法的装饰器。类方法与实例方法不同,它的第一个参数是类本身,而不是实例。类方法使用装饰器@classmethod进行声明。

class MathUtils:
    @classmethod
    def add(cls, x, y):
        return x + y

# 调用类方法
result = MathUtils.add(3, 5)
print(result)  # 输出:8

3.@property

@property是Python中用来定义属性的装饰器。属性是一种特殊的方法,它可以像访问常规属性一样使用点操作符进行访问,而不需要显式调用方法。@property装饰器可以将一个方法转换为只读属性。

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return 2 * self.radius

# 访问属性
circle = Circle(5)
print(circle.diameter)  # 输出:10

4.@classmethod和@property的组合使用

@classmethod和@property可以组合使用,用于定义只读类属性。这种属性既可以通过类名访问,也可以通过实例访问。

class Person:
    name = "John"

    @classmethod
    @property
    def age(cls):
        return 30

# 访问类属性
print(Person.name)  # 输出:John
print(Person.age)  # 输出:30

# 实例化后访问类属性
person = Person()
print(person.name)  # 输出:John
print(person.age)  # 输出:30

5.@abstractmethod

@abstractmethod是Python中用来定义抽象方法的装饰器。抽象方法是一种没有实现的方法,它只作为基类的接口。抽象方法使用装饰器@abstractmethod进行声明。

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# 实例化抽象类会报错
shape = Shape()  # 报错:TypeError: Can't instantiate abstract class Shape with abstract methods area
Python


6.@classmethod和@abstractmethod的组合使用

@classmethod和@abstractmethod可以组合使用,用于定义抽象类方法。抽象类方法是一种既是抽象方法又是类方法的特殊类型。

from abc import ABC, abstractmethod

class Shape(ABC):
    @classmethod
    @abstractmethod
    def area(cls):
        pass

# 实例化抽象类会报错
shape = Shape()  # 报错:TypeError: Can't instantiate abstract class Shape with abstract methods area

常用的装饰器示例


1.缓存结果

使用内存缓存来存储函数结果,避免重复计算


import functools

def cache_decorator(func):
    cache = {}

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key in cache:
            return cache[key]
        else:
            result = func(*args, **kwargs)
            cache[key] = result
            return result

    return wrapper


@cache_decorator
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(10))  # 第一次计算
print(fibonacci(10))  # 从缓存中获取结果,不会重新计算


2.单例模式

判断实例对象是否已经存在,存在则使用已有实例

def singleton(cls):
 '''创建一个单例模式'''
 def single_wrapper(*args, **kwargs):
    if not single_wrapper.instance:
       single_wrapper.instance = cls(*args, **kwargs)
    return single_wrapper.instance
    single_wrapper.instance = None
 return single_wrapper

3.异常重试

使用内存缓存来存储函数结果,避免重复计算

import time
 
 def retry(max_attempts, delay):
    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempts + 1} failed. Retrying in {delay} seconds.")
                    attempts += 1
                    time.sleep(delay)
            raise Exception("Max retry attempts exceeded.")
        return wrapper
    return decorator

 @retry(max_attempts=3, delay=2)
 def fetch_data_from_api(api_url):
    # Your API data fetching code here

4.权限验证装饰器

用于检查用户是否有权限执行某个函数

def permission_decorator(required_permission):
    def decorator(func):
        def wrapper(*args, **kwargs):
            # 假设当前用户有一个权限列表
            current_user_permissions = ["admin", "editor"]
            
            if required_permission in current_user_permissions:
                return func(*args, **kwargs)
            else:
                raise PermissionError(f"You do not have the {required_permission} permission.")
        return wrapper
    return decorator

@permission_decorator("admin")
def admin_only_function():
    return "This function can only be called by admins."

# 假设当前用户有admin权限
try:
    print(admin_only_function())
except PermissionError as e:
    print(e)

总结

装饰器是Python编程中至关重要的工具,它通过巧妙分离横切关注点,显著提升了代码的重用性和灵活性;
深入理解装饰器的核心概念、实用技巧及实现方式,对于提升代码的可读性、可维护性和扩展性至关重要;
掌握装饰器的运用,对于高级编程实践来说不可或缺,它将为开发者在优化代码质量和开发流程中带来无限可能。

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

需要休息的KK.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值