Python实践提升-装饰器

Python装饰器:从入门到精通 - 全面掌握Python类和装饰器的终极指南 想要成为Python高手?掌握装饰器是必经之路!本文将带你深入理解Python装饰器的精髓,从基础概念到高级应用,全方位提升你的Python编程技能。装饰器(Decorator)是Python中一个极其强大的特性,从Python 2.4版本开始引入,并在后续版本中不断完善和增强。它让我们能够优雅地修改或扩展函数和类的行为,而无需改变其源代码。 2.2 内置装饰器 Python提供了几个强大的内置装饰器:类装饰器是Python面向对象编程中的重要工具,它们能够改变类方法的行为和访问方式。以下是类中常用装饰器的 阅读详情

Python实践提升-装饰器
在大约十年前, Python Web 开发相关的工作用的是 Django 框架。那时 Django 是整个 Python 生态圈里最流行的开源 Web 开发框架 。

作为最流行的 Web 开发框架,Django 提供了非常强大的功能。它有一个清晰的 MTV(model-template-view,模型—模板—视图)分层架构和开箱即用的 ORM2 引擎,以及丰富到令人眼花缭乱的可配置项。

2object-relational mapping(对象关系映射)的首字母缩写,指一种把数据库中的数据自动映射为程序内对象的技术,比如执行 User.objects.all() 会自动去数据库查询 user 表,并将所有数据自动转换为 User 对象。

但正因为提供了这些强大的功能,Django 的学习与使用成本也非常高。假如你从来没有接触过 Django,想要用它开发一个 Web 网站,得先学习一大堆框架配置、路由视图相关的东西,一晃大半天就过去了。

在 Django 几乎统治了 Python Web 开发的那段日子里,不知从哪一天开始,越来越多的人突然开始谈论起另一个叫 Flask 的 Web 开发框架。

出于好奇,我点开了 Flask 框架的官方文档,很快就被它的简洁性吸引了。举个例子,使用 Flask 开发一个 Hello World 站点,只需要下面这寥寥几行代码:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

作为对比,假如用 Django 开发这么一个站点,光配置文件 settings.py 里的代码就远比这些多。

虽然在之后的好几个月,在我深入学习使用 Flask 的过程中,发现它有许多值得称道的设计,但在当时,在我刚看到官网的 Hello World 样例代码的那一刻,最吸引我的,其实是那一行路由注册代码:@app.route('/')。

在接触 Flask 之前,虽然我已经使用过装饰器,也自己实现过装饰器,但从来没想过,装饰器原来可以用在 Web 站点中注册访问路由,而且这套 API 看起来居然特别自然、符合直觉。

再后来,我接触到更多和装饰器有关的模块,比如基于装饰器的缓存模块、基于装饰器的命令行工具集 Click 等,如代码清单 8-1 所示。

代码清单 8-1 使用 Click 模块定义的一个简单的命令行工具 3

3通过 click.option() 来定义脚本所需的参数,简单灵活,代码来自官方文档。

import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):  
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

这些模块和工具,无一例外地使用装饰器实现了简单好用的 API,为我的开发工作带来了极大便利。

不过,虽然 Python 里的装饰器(decorator)很有用,但它本身并不复杂,只是 Python 语言的一颗小小的语法糖。如你所知,这样的装饰器应用代码:

@cache
def function():
    ...
  完全等同于下面这样:

def function():
    ...

function = cache(function)

装饰器并不提供任何独特的功能,它所做的,只是让我们可以在函数定义语句上方,直接添加用来修改函数行为的装饰器函数。假如没有装饰器,我们也可以在完成函数定义后,手动做一次包装和重新赋值。

但正是因为装饰器提供的这一丁点儿好处,“通过包装函数来修改函数”这件事变得简单和自然起来。

在日常工作中,如果你掌握了如何编写装饰器,并在恰当的时机使用装饰器,就可以写出更易复用、更好扩展的代码。在本章中,我将分享一些在 Python 中编写装饰器的技巧,以及几个用于编写装饰器的常见工具,希望它们能助你写出更好的代码。

8.1 基础知识
8.1.1 装饰器基础
  装饰器是一种通过包装目标函数来修改其行为的特殊高阶函数,绝大多数装饰器是利用函数的闭包原理实现的。

代码清单 8-2 所示的 timer 是个简单的装饰器,它会记录并打印函数的每次调用耗时。

代码清单 8-2 打印函数耗时的无参数装饰器 timer

def timer(func):
    """装饰器:打印函数耗时"""

    def decorated(*args, **kwargs):
        st = time.perf_counter()
        ret = func(*args, **kwargs)
        print('time cost: {} seconds'.format(time.perf_counter() - st))
        return ret

    return decorated

在上面的代码中,timer 装饰器接收待装饰函数 func 作为唯一的位置参数,并在函数内定义了一个新函数:decorated。

在写装饰器时,我一般把 decorated 叫作“包装函数”。这些包装函数通常接收任意数目的可变参数 (*args, **kwargs),主要通过调用原始函数 func 来完成工作。在包装函数内部,常会增加一些额外步骤,比如打印信息、修改参数等。

当其他函数应用了 timer 装饰器后,包装函数 decorated 会作为装饰器的返回值,完全替换被装饰的原始函数 func。

random_sleep() 使用了 timer 装饰器:

@timer
def random_sleep():
    """随机睡眠一小会儿"""
    time.sleep(random.random())

调用结果如下:

>>> random_sleep()
time cost: 0.8360576540000002 seconds ➊

❶ 由 timer 装饰器打印的耗时信息

timer 是一个无参数装饰器,实现起来较为简单。假如你想实现一个接收参数的装饰器,代码会更复杂一些。

代码清单 8-3 给 timer 增加了额外的 print_args 参数。

代码清单 8-3 增加 print_args 的有参数装饰器 `timer

def timer(print_args=False):
    """装饰器:打印函数耗时

    :param print_args: 是否打印方法名和参数,默认为 False
    """

    def decorator(func):
        def wrapper(*args, **kwargs):
            st = time.perf_counter()
            ret = func(*args, **kwargs)
            if print_args:
                print(f'"{func.__name__}", args: {args}, kwargs: {kwargs}')
            print('time cost: {} seconds'.format(time.perf_counter() - st))
            return ret

        return wrapper

    return decorator

可以看到,为了增加对参数的支持,装饰器在原本的两层嵌套函数上又加了一层。这是由于整个装饰过程发生了变化所导致的。

具体来说,下面的装饰器应用代码:

@timer(print_args=True)
def random_sleep(): ...

展开后等同于下面的调用:

_decorator = timer(print_args=True) ➊
random_sleep = _decorator(random_sleep) ➋

❶ 先进行一次调用,传入装饰器参数,获得第一层内嵌函数 decorator

❷ 进行第二次调用,获取第二层内嵌函数 wrapper

在应用有参数装饰器时,一共要做两次函数调用,所以装饰器总共得包含三层嵌套函数。正因为如此,有参数装饰器的代码一直都难写、难读。但不要紧,在 8.1.4 节中,我会介绍如何用类来实现有参数装饰器,减少代码的嵌套层级。

8.1.2 使用 functools.wraps() 修饰包装函数
  在装饰器包装目标函数的过程中,常会出现一些副作用,其中一种是丢失函数元数据。

在前一节的例子里,我用 timer 装饰了 random_sleep() 函数。现在,假如我想读取 random_sleep() 函数的名称、文档等属性,就会碰到一件尴尬的事情——函数的所有元数据都变成了装饰器的内层包装函数 decorated 的值:

>>> random_sleep.__name__
'decorated'
>>> print(random_sleep.__doc__)
None

对于装饰器来说,上面的元数据丢失问题只能算一个常见的小问题。但如果你的装饰器会做一些更复杂的事,比如为原始函数增加额外属性(或函数)等,那你就会踏入一个更大的陷阱。

举个例子,现在有一个装饰器 calls_counter,专门用来记录函数一共被调用了多少次,并且提供一个额外的函数来打印总次数,如代码清单 8-4 所示。

代码清单 8-4 记录函数调用次数的装饰器

calls_counter

def calls_counter(func):
    """装饰器:记录函数被调用了多少次

    使用 func.print_counter() 可以打印统计到的信息
    """
    counter = 0

    def decorated(*args, **kwargs):
        nonlocal counter
        counter += 1
        return func(*args, **kwargs)

    def print_counter():
        print(f'Counter: {counter}')

    decorated.print_counter = print_counter ➊
    return decorated

❶ 为被装饰函数增加额外函数,打印统计到的调用次数

装饰器的执行效果如下:

>>> random_sleep()
>>> random_sleep()
>>> random_sleep.print_counter()
Counter: 2

在单独使用 calls_counter 装饰器时,程序可以正常工作。但是,当你把前面的 timer 与 calls_counter 装饰器组合在一起使用时,就会出现问题:

@timer
@calls_counter
def random_sleep():
    """随机睡眠一小会儿"""
    time.sleep(random.random())

调用效果如下:

>>> random_sleep()
function took: 0.36080002784729004 seconds

>>> random_sleep.print_counter()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'print_counter'

虽然 timer 装饰器仍在工作,函数执行时会打印耗时信息,但本该由 calls_counter 装饰器给函数追加的 print_counter 属性找不到了。

为了分析原因,首先我们得把上面的装饰器调用展开成下面这样的语句:

random_sleep = calls_counter(random_sleep) ➊
random_sleep = timer(random_sleep) ➋

❶ 首先,由 calls_counter 对函数进行包装,此时的 random_sleep 变成了新的包装函数,包含 print_counter 属性

❷ 使用 timer 包装后,random_sleep 变成了 timer 提供的包装函数,原包装函数额外的 print_counter 属性被自然地丢掉了

要解决这个问题,我们需要在装饰器内包装函数时,保留原始函数的额外属性。而 functools 模块下的 wraps() 函数正好可以完成这件事情。

使用 wraps(),装饰器只需要做一点儿改动:

from functools import wraps


def timer(func):

    @wraps(func) ➊
    def decorated(*args, **kwargs):
        ...

    return decorated

❶ 添加 @wraps(wrapped) 来装饰 decorated 函数后,wraps() 首先会基于原函数 func 来更新包装函数 decorated 的名称、文档等内置属性,之后会将 func 的所有额外属性赋值到 decorated 上

在 timer 和 calls_counter 装饰器里增加 wraps 后,前面的所有问题都可以得到圆满的解决。

首先,被装饰函数的名称和文档等元数据会保留:

>>> random_sleep.__name__
'random_sleep'
>>> random_sleep.__doc__
'随机睡眠一小会儿'

calls_counter 装饰器为函数追加的额外函数也可以正常访问了:

>>> random_sleep()
function took: 0.9187359809875488 seconds
>>> random_sleep()
function took: 0.8986420631408691 seconds
>>> random_sleep.print_counter()
Counter: 2

正因为如此,在编写装饰器时,切记使用 @functools.wraps() 来修饰包装函数。

8.1.3 实现可选参数装饰器
  假如你用嵌套函数来实现装饰器,接收参数与不接收参数的装饰器代码有很大的区别——前者总是比后者多一层嵌套。

# 1. 接收参数的装饰器:2 层嵌套
def delayed_start(duration=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return decorator

# 2. 不接收参数的装饰器:1 层嵌套
def delayed_start(func):
    def wrapper(*args, **kwargs):
        ...
    return wrapper

当你实现了一个接收参数的装饰器后,即便所有参数都是有默认值的可选参数,你也必须在使用装饰器时加上括号:

@delayed_start(duration=2) ➊

@delayed_start() ➋

❶ 使用装饰器时提供参数

❷ 不提供参数,也需要使用括号调用装饰器

有参数装饰器的这个特点提高了它的使用成本——如果使用者忘记添加那对括号,程序就会出错。

那么有没有什么办法,能让我们省去那对括号,直接使用 @delayed_start 这种写法呢?答案是肯定的,利用仅限关键字参数,你可以很方便地做到这一点。

代码清单 8-5 里的 delayed_start 装饰器就定义了可选的 duration 参数。

代码清单 8-5 定义了可选参数的装饰器 delayed_start

def delayed_start(func=None, *, duration=1): ➊
    """装饰器:在执行被装饰函数前,等待一段时间

    :param duration: 需要等待的秒数
    """

    def decorator(_func):
        def wrapper(*args, **kwargs):
            print(f'Wait for {duration} second before starting...')
            time.sleep(duration)
            return _func(*args, **kwargs)

        return wrapper
    if func is None: ➋
        return decorator
    else:
        return decorator(func) ➌

❶ 把所有参数都变成提供了默认值的可选参数

❷ 当 func 为 None 时,代表使用方提供了关键字参数,比如 @delayed_start(duration=2),此时返回接收单个函数参数的内层子装饰器 decorator

❸ 当位置参数 func 不为 None 时,代表使用方没提供关键字参数,直接用了无括号的 @ delayed_start 调用方式,此时返回内层包装函数 wrapper

这样定义装饰器以后,我们可以通过多种方式来使用它:

# 1. 不提供任何参数
@delayed_start
def hello(): ...

# 2. 提供可选的关键字参数
@delayed_start(duration=2)
def hello(): ...

# 3. 提供括号调用,但不提供任何参数
@delayed_start()
def hello(): ...

把参数变为可选能有效降低使用者的心智负担,让装饰器变得更易用。标准库 dataclasses 模块里的 @dataclass 装饰器就使用了这个小技巧。
8.1.4 用类来实现装饰器(函数替换)
  绝大多数情况下,我们会选择用嵌套函数来实现装饰器,但这并非构造装饰器的唯一方式。事实上,某个对象是否能通过装饰器(@decorator)的形式使用只有一条判断标准,那就是 decorator 是不是一个可调用的对象。

函数自然是可调用对象,除此之外,类同样也是可调用对象。

>>> class Foo:
...     pass
...
>>> callable(Foo) ➊
True

❶ 使用 callable() 内置函数可以判断某个对象是否可调用

如果一个类实现了 call 魔法方法,那么它的实例也会变成可调用对象:

>>> class Foo:
...     def __call__(self, name): ➊
...         print(f'Hello, {name}')
...
>>> foo = Foo()
>>> callable(foo)
True
>>> foo('World') ➋
Hello, World

❶ call 魔法方法是用来实现可调用对象的关键方法

❷ 调用类实例时,可以像调用普通函数一样提供额外参数

基于类的这些特点,我们完全可以用它来实现装饰器。

如果按装饰器用于替换原函数的对象类型来分类,类实现的装饰器可分为两种,一种是“函数替换”,另一种是“实例替换”。下面我们先来看一下前者。

函数替换装饰器虽然是基于类实现的,但用来替换原函数的对象仍然是个普通的包装函数。这种技术最适合用来实现接收参数的装饰器。

代码清单 8-6 用类的方式重新实现了接收参数的 timer 装饰器。

代码清单 8-6 用类实现的 timer 装饰器

class timer:
    """装饰器:打印函数耗时

    :param print_args: 是否打印方法名和参数,默认为 False
    """

    def __init__(self, print_args):
        self.print_args = print_args

    def __call__(self, func):
        @wraps(func)
        def decorated(*args, **kwargs):
            st = time.perf_counter()
            ret = func(*args, **kwargs)
            if self.print_args:
                print(f'"{func.__name__}", args: {args}, kwargs: {kwargs}')
            print('time cost: {} seconds'.format(time.perf_counter() - st))
            return ret

        return decorated

还记得我之前说过,有参数装饰器一共得提供两次函数调用吗?通过类实现的装饰器,其实就是把原本的两次函数调用替换成了类和类实例的调用。

(1) 第一次调用:_deco = timer(print_args=True) 实际上是在初始化一个 timer 实例。

(2) 第二次调用:func = _deco(func) 是在调用 timer 实例,触发 call 方法。

相比三层嵌套的闭包函数装饰器,上面这种写法在实现有参数装饰器时,代码更清晰一些,里面的嵌套也少了一层。不过,虽然装饰器是用类实现的,但最终用来替换原函数的对象,仍然是一个处在 call 方法里的闭包函数 decorated。

虽然“函数替换”装饰器的代码更简单,但它和普通装饰器并没有本质区别。下面我会介绍另一种更强大的装饰器——用实例来替换原函数的“实例替换”装饰器。

8.1.5 用类来实现装饰器(实例替换)
  和“函数替换”装饰器不一样,“实例替换”装饰器最终会用一个类实例来替换原函数。通过组合不同的工具,它既能实现无参数装饰器,也能实现有参数装饰器。

实现无参数装饰器

用类来实现装饰器时,被装饰的函数 func 会作为唯一的初始化参数传递到类的实例化方法 init 中。同时,类的实例化结果——类实例(class instance),会作为包装对象替换原始函数。

代码清单 8-7 实现了一个延迟函数执行的装饰器。

代码清单 8-7 实例替换的无参数装饰器 DelayedStart

class DelayedStart:
    """在执行被装饰函数前,等待 1 秒钟"""

    def __init__(self, func):
        update_wrapper(self, func) ➊
        self.func = func

    def __call__(self, *args, **kwargs): ➋
        print(f'Wait for 1 second before starting...')
        time.sleep(1)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs): ➌
        """跳过等待,立刻执行被装饰函数"""
        print('Call without delay')
        return self.func(*args, **kwargs)

❶ update_wrapper 与前面的 wraps 一样,都是把被包装函数的元数据更新到包装者(在这里是 DelayedStart 实例)上

❷ 通过实现 call 方法,让 DelayedStart 的实例变得可调用,以此模拟函数的调用行为

❸ 为装饰器类定义额外方法,提供更多样化的接口

执行效果如下:

>>> @DelayedStart
... def hello():
...     print("Hello, World.")

>>> hello
<__main__.DelayedStart object at 0x100b71130>
>>> type(hello)
<class '__main__.DelayedStart'>
>>> hello.__name__ ➊
'hello'

>>> hello() ➋
Wait for 1 second before starting...
Hello, World.
>>> hello.eager_call() ➌
Call without delay
Hello, World.

❶ 被装饰的 hello 函数已经变成了装饰器类 DelayedStart 的实例,但是因为 update_wrapper 的作用,这个实例仍然保留了被装饰函数的元数据

❷ 此时触发的其实是装饰器类实例的 call 方法

❸ 使用额外的 eager_call 接口调用函数

实现有参数装饰器

同普通装饰器一样,“实例替换”装饰器也可以支持参数。为此我们需要先修改类的实例化方法,增加额外的参数,再定义一个新函数,由它来负责基于类创建新的可调用对象,这个新函数同时也是会被实际使用的装饰器。

在代码清单 8-8 中,我为 DelayedStart 增加了控制调用延时的 duration 参数,并定义了 delayed_start() 函数。

代码清单 8-8 实例替换的有参数装饰器 delayed_start

class DelayedStart:
    """在执行被装饰函数前,等待一段时间
    :param func: 被装饰的函数
    :param duration: 需要等待的秒数
    """

    def __init__(self, func, *, duration=1): ➊
        update_wrapper(self, func)
        self.func = func
        self.duration = duration

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} second before starting...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs): ...

def delayed_start(**kwargs):
    """装饰器:推迟某个函数的执行"""
    return functools.partial(DelayedStart, **kwargs) ➋

❶ 把 func 参数以外的其他参数都定义为“仅限关键字参数”,从而更好地区分原始函数与装饰器的其他参数

❷ 通过 partial 构建一个新的可调用对象,这个对象接收的唯一参数是待装饰函数 func,因此可以用作装饰器

使用样例如下:

@delayed_start(duration=2)
def hello():
    print("Hello, World.")

相比传统做法,用类来实现装饰器(实例替换)的主要优势在于,你可以更方便地管理装饰器的内部状态,同时也可以更自然地为被装饰对象追加额外的方法和属性。

8.1.6 使用 wrapt 模块助力装饰器编写
  在编写通用装饰器时,我常常会遇到一类麻烦事。

如代码清单 8-9 所示,我实现了一个自动注入函数参数的装饰器 provide_number,它在装饰函数后,会在后者被调用时自动生成一个随机数,并将其注入为函数的第一个位置参数。

代码清单 8-9 注入数字的装饰器 provide_number

import random

def provide_number(min_num, max_num):
    """
    装饰器:随机生成一个在 [min_num, max_num] 范围内的整数,
    并将其追加为函数的第一个位置参数
    """

    def wrapper(func):
        def decorated(*args, **kwargs):
            num = random.randint(min_num, max_num)
            # 将 num 追加为第一个参数,然后调用函数
            return func(num, *args, **kwargs)

        return decorated

    return wrapper

使用效果如下:

>>> @provide_number(1, 100)
... def print_random_number(num):
...     print(num)
...
>>> print_random_number()
57

@provide_number 装饰器的功能看上去很不错,但当我用它来修饰类方法时,就会碰上“麻烦事”:

>>> class Foo:
...     @provide_number(1, 100)
...     def print_random_number(self, num):
...         print(num)
...
>>> Foo().print_random_number()
<__main__.Foo object at 0x100f70460>
print_random_number()

如你所见,类实例中的 print_random_number() 方法并没有打印我期望中的随机数字 num,而是输出了类实例 self 对象。

这是因为类方法(method)和函数(function)在工作机制上有细微的区别。当类实例方法被调用时,第一个位置参数总是当前绑定的类实例 self 对象。因此,当装饰器向 *args 前追加随机数时,其实已经把 *args 里的 self 挤到了 num 参数所在的位置,从而导致了上面的问题。

为了修复这个问题,provide_number 装饰器在追加位置参数时,必须聪明地判断当前被修饰的对象是普通函数还是类方法。假如被修饰的对象是类方法,那就得跳过藏在 *args 里的类实例变量,才能正确将 num 作为第一个参数注入。

假如要手动实现这个判断,装饰器内部必须增加一些烦琐的兼容代码,费工费时。幸运的是,wrapt 模块可以帮我们轻松处理好这类问题。

wrapt 是一个第三方装饰器工具库,利用它,我们可以非常方便地改造 provide_number 装饰器,完美地解决这个问题。

使用 wrapt 改造过的装饰器如代码清单 8-10 所示。

代码清单 8-10 基于 wrapt 模块实现的 provide_number 装饰器

import wrapt


def provide_number(min_num, max_num):
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        # 参数含义:
        #
        # - wrapped:被装饰的函数或类方法
        # - instance:
        # - 如果被装饰者为普通类方法,则该值为类实例
        # - 如果被装饰者为 classmethod 类方法,则该值为类
        # - 如果被装饰者为类/函数/静态方法,则该值为 None
        #
        # - args:调用时的位置参数(注意没有 * 符号)
        # - kwargs:调用时的关键字参数(注意没有 ** 符号)
        #
        num = random.randint(min_num, max_num)
        # 无须关注 wrapped 是类方法还是普通函数,直接在头部追加参数
        args = (num,) + args
        return wrapped(*args, **kwargs)

return wrapper

新装饰器可以完美兼容普通函数与类方法两种情况:

>>> print_random_number()
22
>>> Foo().print_random_number()
93

使用 wrapt 模块编写的装饰器,除了解决了类方法兼容问题以外,代码嵌套层级也比普通装饰器少,变得更扁平、更易读。如果你有兴趣,可以参阅 wrapt 模块的官方文档了解更多信息。

8.2 编程建议
8.2.1 了解装饰器的本质优势
  当我们向其他人介绍装饰器时,常常会说:“装饰器为我们提供了一种动态修改函数的能力。”这么说有一定道理,但是并不准确。“动态修改函数”的能力,其实并不是由装饰器提供的。假如没有装饰器,我们也能在定义完函数后,手动调用装饰函数来修改它。

装饰器带来的改变,主要在于把修改函数的调用提前到了函数定义处,而这一点儿位置上的小变化,重塑了读者理解代码的整个过程。

比如,当人们读到下面的函数定义语句时,马上就能明白:“哦,原来这个视图函数需要登录才能访问。”

@login_requried
def view_function(request):
    ...

所以,装饰器的优势并不在于它提供了动态修改函数的能力,而在于它把影响函数的装饰行为移到了函数头部,降低了代码的阅读与理解成本。

为了充分发挥这个优势,装饰器特别适合用来实现以下功能。

(1) 运行时校验:在执行阶段进行特定校验,当校验通不过时终止执行。

适合原因:装饰器可以方便地在函数执行前介入,并且可以读取所有参数辅助校验。
代表样例:Django 框架中的用户登录态校验装饰器 @login_required。
  (2) 注入额外参数:在函数被调用时自动注入额外的调用参数。

适合原因:装饰器的位置在函数头部,非常靠近参数被定义的位置,关联性强。
代表样例:unittest.mock 模块的装饰器 @patch。
  (3) 缓存执行结果:通过调用参数等输入信息,直接缓存函数执行结果。

适合原因:添加缓存不需要侵入函数内部逻辑,并且功能非常独立和通用。
代表样例:functools 模块的缓存装饰器 @lru_cache。
  (4) 注册函数:将被装饰函数注册为某个外部流程的一部分。

适合原因:在定义函数时可以直接完成注册,关联性强。
代表样例:Flask 框架的路由注册装饰器 @app.route。
  (5) 替换为复杂对象:将原函数(方法)替换为更复杂的对象,比如类实例或特殊的描述符对象(见 12.1.3 节)。

适合原因:在执行替换操作时,装饰器语法天然比 foo = staticmethod(foo) 的写法要直观得多。
代表样例:静态类方法装饰器 @staticmethod。
  在设计新的装饰器时,你可以先参考上面的常见装饰器功能列表,琢磨琢磨自己的设计是否能很好地发挥装饰器的优势。切勿滥用装饰器技术,设计出一些天马行空但难以理解的 API。吸取前人经验,同时在设计上保持克制,才能写出更好用的装饰器。

8.2.2 使用类装饰器替代元类
  Python 中的元类(metaclass)是一种特殊的类。就像类可以控制实例的创建过程一样,元类可以控制类的创建过程。通过元类,我们能实现各种强大的功能。比如下面的代码就利用元类统一注册所有 Validator 类:

_validators = {}

class ValidatorMeta(type):
    """元类:统一注册所有校验器类,方便后续使用"""

    def __new__(cls, name, bases, attrs):
        ret = super().__new__(cls, name, bases, attrs)
        _validators[attrs['name']] = ret
        return ret


class StringValidator(metaclass=ValidatorMeta):
    name = 'string'

class IntegerValidator(metaclass=ValidatorMeta):
    name = 'int'

查看注册结果:

>>> _validators
{'string': <class '__main__.StringValidator'>, 'int': <class '__main__.IntegerValidator'>}

虽然元类的功能很强大,但它的学习与理解成本非常高。其实,对于实现上面这种常见需求,并不是非使用元类不可,使用类装饰器也能非常方便地完成同样的工作。

类装饰器的工作原理与普通装饰器类似。下面的代码就用类装饰器实现了 ValidatorMeta 元类的功能:

def register(cls):
    """装饰器:统一注册所有校验器类,方便后续使用"""
    _validators[cls.name] = cls
    return cls

@register
class StringValidator:
    name = 'string'

@register
class IntegerValidator:
    name = 'int'

相比元类,使用类装饰器的代码要容易理解得多。

除了上面的注册功能以外,你还可以用类装饰器完成许多实用的事情,比如实现单例设计模式、自动为类追加方法,等等。

虽然类装饰器并不能覆盖元类的所有功能,但在许多场景下,类装饰器可能比元类更合适,因为它不光写起来容易,理解起来也更简单。像广为人知的标准库模块 dataclasses 里的 @ dataclass 就选择了类装饰器,而不是元类。

8.2.3 别弄混装饰器和装饰器模式
  1994 年出版的经典软件开发著作《设计模式:可复用面向对象软件的基础》中,一共介绍了 23 种经典的面向对象设计模式。这些设计模式为编写好代码提供了许多指导,影响了一代又一代的程序员。

在这 23 种设计模式中,有一种“装饰器模式”。也许是因为装饰器模式和 Python 里的装饰器使用了同一个名字:装饰器(decorator),导致经常有人把它俩当成一回事儿,认为使用 Python 里的装饰器就是在实践装饰器模式。

但事实上,《设计模式》一书中的“装饰器模式”与 Python 里的“装饰器”截然不同。

装饰器模式属于面向对象领域。实现装饰器模式,需要具备以下关键要素:

设计一个统一的接口;
编写多个符合该接口的装饰器类,每个类只实现一个简单的功能;
通过组合的方式嵌套使用这些装饰器类;
通过类和类之间的层层包装来实现复杂的功能。
  代码清单 8-11 是我用 Python 实现的一个简单的装饰器模式。

代码清单 8-11 装饰器模式示例

class Numbers:
    """一个包含多个数字的简单类"""

    def __init__(self, numbers):
        self.numbers = numbers

    def get(self):
        return self.numbers


class EvenOnlyDecorator:
    """装饰器类:过滤所有偶数"""

    def __init__(self, decorated):
        self.decorated = decorated

    def get(self):
        return [num for num in self.decorated.get() if num % 2 == 0]


class GreaterThanDecorator:
    """装饰器类:过滤大于某个数的数"""

    def __init__(self, decorated, min_value):
        self.decorated = decorated
        self.min_value = min_value

    def get(self):
        return [num for num in self.decorated.get() if num > self.min_value]


obj = Numbers([42, 12, 13, 17, 18, 41, 32])
even_obj = EvenOnlyDecorator(obj)
gt_obj = GreaterThanDecorator(even_obj, min_value=30)
print(gt_obj.get())

执行结果如下:

[42, 32]

从上面的代码中你能发现,装饰器模式和 Python 里的装饰器毫不相干。如果硬要找一点儿联系,它俩可能都和“包装”有关——一个包装函数,另一个包装类。

所以,请不要混淆装饰器和装饰器模式,它们只是名字里刚好都有“装饰器”而已。

8.2.4 浅装饰器,深实现
  在编写装饰器时,人们很容易产生这样的想法:“我的装饰器要实现某个功能,所以我要把所有逻辑都放在装饰器里实现。”抱着这样的想法去写代码,很容易写出异常复杂的装饰器代码。

在编写了许多装饰器后,我发现了一种更好的代码组织思路,那就是:浅装饰器,深实现。

举个例子,流行的第三方命令行工具包 Click 里大量使用了装饰器。但如果你查看 Click 包的源码,就会发现 Click 的所有装饰器都在一个不到 400 行代码的 decorators.py 文件中,里面的大部分装饰器的代码不超过 10 行,如代码清单 8-12 所示。

代码清单 8-12 @click.command 装饰器源码

def command(name=None, cls=None, **attrs):
    if cls is None:
        cls = Command

    def decorator(f):
        cmd = _make_command(f, name, attrs, cls)
        cmd.__doc__ = f.__doc__
        return cmd

    return decorator

即便是 Click 的核心装饰器 @command,也只有短短 8 行代码。它所做的,只是简单地把被装饰函数替换为 Command 实例,而所有核心逻辑都在 Command 实例中。

这样的装饰器很浅,只做一些微小的工作,但这样的代码扩展性其实更强。

因为归根结底,装饰器其实只是一类特殊的 API,一种提供服务的方式。比起把所有核心逻辑都放在装饰器内,不如让装饰器里只有一层浅浅的包装层,而把更多的实现细节放在其他函数或类中。

这样做之后,假如你未来需要为模块增加装饰器以外的其他 API,比如上下文管理器,就会发现自己之前写的大部分核心代码仍然可以复用,因为它们并没有和装饰器耦合。

8.3 总结
  在本章中,我分享了一些与装饰器有关的知识。

装饰器是 Python 为我们提供的一颗语法糖,它和“装饰器模式”没有任何关系。任何可调用对象都可以当作装饰器来使用,因此,除了最常见的用嵌套函数来实现装饰器外,我们也可以用类来实现装饰器。

在装饰器包装原始函数的过程中,会产生“元数据丢失”副作用,你可以通过 functools.wraps() 来解决这个问题。

用类实现的装饰器分为两种:“函数替换”与“实例替换”。后者可以有效地实现状态管理、追加行为功能。在实现有参数“实例替换”装饰器时,你需要定义一个额外的函数来配合装饰器类。

在编写装饰器时,第三方工具包 wrapt 非常有用,借助它能写出更扁平的装饰器,也更容易兼容装饰函数与类方法两种场景。

装饰器是一个有趣且非常独特的语言特性。虽然它不提供什么无法替代的功能,但在 API 设计领域给了我们非常大的想象空间。发挥想象力,同时保持克制,也许这就是设计出人人喜爱的装饰器的秘诀。

以下是本章要点知识总结。

(1) 基础与技巧

装饰器最常见的实现方式,是利用闭包原理通过多层嵌套函数实现
在实现装饰器时,请记得使用 wraps() 更新包装函数的元数据
wraps() 不光可以保留元数据,还能保留包装函数的额外属性
利用仅限关键字参数,可以很方便地实现可选参数的装饰器
  (2) 使用类来实现装饰器

只要是可调用的对象,都可以用作装饰器
实现了 call 方法的类实例可调用
基于类的装饰器分为两种:“函数替换”与“实例替换”
“函数替换”装饰器与普通装饰器没什么区别,只是嵌套层级更少
通过类来实现“实例替换”装饰器,在管理状态和追加行为上有天然的优势
混合使用类和函数来实现装饰器,可以灵活满足各种场景
  (3) 使用 wrapt 模块

使用 wrapt 模块可以方便地让装饰器同时兼容函数和类方法
使用 wrapt 模块可以帮你写出结构更扁平的装饰器代码
  (4) 装饰器设计技巧

装饰器将包装调用提前到了函数被定义的位置,它的大部分优点也源于此
在编写装饰器时,请考虑你的设计是否能很好发挥装饰器的优势
在某些场景下,类装饰器可以替代元类,并且代码更简单
装饰器和装饰器模式截然不同,不要弄混它们
装饰器里应该只有一层浅浅的包装代码,要把核心逻辑放在其他函数与类中

AutoDL上传数据详细步骤(自己用的步骤,可能没有其他大佬用的那么高级) 阅读详情

相关推荐

光耦合MOSFET(Optocoupled MOSFET,简称OCMOS)

光耦合MOSFET(Optocoupled MOSFET,简称OCMOS)是一种将光电耦合器和MOSFET结合在一起的器件,用于实现电气隔离和信号传输。选择合适的光耦合MOSFET时,还需要考虑具体应用的需求和环境条件,以确保最佳性能和可靠性。- 最大输出电流和电压:确保所选型号能够满足电路的最大电流和电压需求。- 最大输出电流和电压:确保所选型号能够满足电路的最大电流和电压需求。- 隔离电压:根据应用环境的电气隔离要求选择合适的隔离电压。- 隔离电压:根据应用环境的电气隔离要求选择合适的隔离电压。

eydj2008的专栏 1725

Python各种类型装饰器详解说明

装饰器说明 Python中的装饰器是一种可以装饰其它对象的工具。 该工具本质上是一个可调用的对象(callable),所以装饰器一般可以由函数、类来实现。 装饰器本身需要接受一个被装饰的对象作为参数,该参数通常为函数、方法、类等对象。 装饰器需要返回一个对象,该对象可以是 经过处理的原参数对象、一个包装且类似原参数的对象;或者返回一个不相干内容(通常不建议使用) 相信通过上述一段文字的描述...

上帝De助手 1万+

H3C交换机Console口连接全攻略:从接线到PuTTY配置(附常见问题排查)

本文详细介绍了H3C交换机Console口的连接与配置方法,包括硬件线缆选择、驱动安装、PuTTY参数设置及常见问题排查。特别针对Console转USB线缆的兼容性和驱动安装难点提供实用建议,帮助网络工程师快速掌握串联调试技巧,解决连接中的黑屏、乱码等典型问题。

weixin_42549451的博客 674

python 装饰器有哪些_Python装饰器

一, 引用二, 基本概念问题1:装饰器是什么?解答:严格来说,装饰器只是语法糖, 装饰器是可调用的对象,可以像常规的可调用对象那样调用,特殊的地方是装饰器的参数是一个函数问题2:装饰器有什么特性?解答: 装饰器有2个特性,一是可以把被装饰的函数替换成其他函数, 二是可以在加载模块时候立即执行def decorate(func):print('running decorate', func)def ...

weixin_39870092的博客 1145

基于YOLO+DeepSeek的农作物病虫害检测系统

本系统是一个基于YOLO+DeepSeek的农作物病虫害检测系统。 可使用YOLOV1-YOLOV12的任意模型进行目标检测。可以实现检测图片、批量检测图片、视频检测、摄像头检测四种方式。

qq_51977316的博客 1809

从零开始:如何在STM32上搭建QP框架实现事件驱动开发(附完整代码)

本文详细介绍了如何在STM32嵌入式平台上从零开始搭建QP框架,实现事件驱动开发。通过对比传统前后台系统与RTOS的局限,深入剖析了QP框架基于活动对象和层次式状态机的核心优势,并提供了完整的环境配置、代码移植及一个智能灯光控制器的实战案例,帮助开发者构建清晰、可维护的复杂嵌入式应用。

weixin_29200741的博客 487

python 装饰器

python 装饰器功能以及实现讲解

qq_43031234的博客 1030

数据预处理的分箱操作

数据分箱 介绍 我们在建立模型前,一般需要对特征变量进行离散化,特征离散化后,模型会更稳定,降低模型过拟合的风险。尤其是采用 logsitic 建立评分卡模型时,必须对连续变量进行离散化。而特征离散化处理通常采用的就是分箱法,数据分箱(也称为离散分箱或分段)是一种数据预处理技术,用于减少次要观察误差的影响,提高泛化性。 数据分箱又分为有监督分箱和无监督分箱,是否使用标签进行离散化(分箱)决定了有监督还是无监督的离散化方法。 知识点 无监督分箱 split 分箱 merge 分箱 无监督分箱 这里为了

随笔记 5334

深入解析Python中的装饰器简化代码并提升效率

首先,应使用functools.wraps装饰器来更新包装函数的元数据(如__name__、__doc__),这有助于调试和保持函数的身份信息。其次,考虑设计可接受参数的装饰器,这需要通过嵌套函数来创建装饰器工厂,以提供更大的灵活性。最后,应谨慎使用装饰器,避免过度嵌套导致代码可读性下降,并注意装饰器可能引入的性能开销,特别是在频繁调用的函数上。例如,一个简单的日志记录装饰器可以在函数执行前后自动打印日志,而无需在每个函数中重复编写日志代码,这不仅减少了代码冗余,也提升了开发效率。

ayuzy07的博客 391

C#上位机案例:西门子S7协议通信+实时曲线显示(WinForm+ZedGraph)

创建/// PLC数据模型(对应DB1块)/// 采集时间(用于曲线的X轴)set;/// 温度值(℃)set;/// 压力值(MPa)set;/// PLC通信状态set;西门子S7协议通信和实时曲线显示,也是工控现场最常见的场景之一。整个开发过程的关键不在于“写出代码”,而在于“写出符合工控场景的稳定代码”——比如解耦通信逻辑与界面逻辑、处理跨线程问题、限制数据点数量、释放资源等。新手在学习时,建议先跟着步骤搭建好环境,跑通基础功能,再逐步尝试进阶优化。需要注意的是,工控上位机的核心是。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1342

史上最详细的sqlmap使用教程

sqlmapsqlmap是一个自动化的SQL注入工具,其主要功能是扫描,发现并利用给定的URL进行SQL注入。目前支持的数据库有MySql、Oracle、Access、PostageSQL、SQL Server、IBM DB2、SQLite、Firebird、Sybase和SAP MaxDB等Sqlmap采用了以下5种独特的SQL注入技术基于布尔类型的盲注,即可以根据返回页面判断条件真假的注入。

大河之犬的博客 5万+

Python基础-面向对象编程之装饰器

函数、方法或类定义的前面可以使用一个特殊的符号,称为装饰器,其目的是修改定义后面的行为。

hubing_hust的专栏 1万+

在zcu102开发板上使用PetaLinux运行Qt程序

请注意,上述步骤只是一个概述,并且可能需要根据特定的开发环境和要求进行适当的调整。如果你是一个初学者或者对于这些领域不太熟悉,建议在尝试之前阅读相关的文档和教程,或者寻求专业人士的帮助。在"Projects"菜单中,选择"Build & Run"选项,并在"Build"步骤中选择正确的工具链。注意:如果Qt程序依赖于其他库文件,需要将这些库文件一起复制到PetaLinux工程的根文件系统目录中,并在Qt程序中正确地配置库文件路径。如果一切顺利,PetaLinux系统将启动,并自动运行Qt程序。

UhxzCombined的博客 698

Python装饰器深度解析:原理、应用与最佳实践

本文系统介绍了Python装饰器的原理与应用。首先阐述了装饰器的本质是高阶函数,能够在不修改原函数代码的情况下为其添加新功能。文章详细讲解了装饰器的基础语法、工作原理和进阶技巧,包括处理函数参数、保留元信息、带参数装饰器等。重点展示了装饰器的实际应用场景,如性能计时、缓存、权限验证等,并提供了最佳实践建议。最后分析了常见陷阱和高级主题,如单例模式实现和装饰器工厂。装饰器是Python强大而优雅的特性,合理使用可以显著提升代码的可读性和可维护性。

exlink2012的专栏 883

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Python 装饰器 (Decorators):深度解析、应用与最佳实践

Python装饰器是一种高级函数,允许在不修改原函数代码的情况下为其添加额外功能。其核心思想是利用Python函数作为"一等公民"的特性(可赋值、传递、返回),通过闭包和内部函数实现功能扩展。装饰器基本形式包含一个接收函数并返回新函数的函数,使用@语法糖简化应用。进阶用法包括处理函数参数的*args和**kwargs,以及通过多层嵌套实现带参数的装饰器。装饰器能优雅地实现性能分析、权限控制、日志记录等横切关注点,提高代码复用性和可维护性。

weixin_42107409的博客 1350

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Python技术进阶掌握装饰器提升代码效率

当使用装饰器时,实际上是定义了一个外层函数(即装饰器本身),它接收被装饰的函数作为参数,然后在其内部定义一个新的包装函数。首要的是使用`functools.wraps`装饰器来更新包装函数的元数据(如`__name__`, `__doc__`等),这对于调试和序列化至关重要。`__init__`方法接收被装饰的函数,而`__call__`方法使得类的实例可以像函数一样被调用,从而充当包装函数的角色。对于异步函数(使用`async def`定义),我们需要定义异步装饰器,其内部包装函数也应是异步的。

juyuesf的专栏 884

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

Python装饰器深度解析:提升代码可读性与复用性

Python 装饰器(Decorator)是提升代码可读性与复用性的强大工具。无论是日志记录、权限校验、性能分析还是缓存机制,装饰器都能让你的代码更加优雅、简洁和高效。本文将深入解析 Python 装饰器的原理、常见用法、进阶技巧与最佳实践,助你写出更具专业水准的 Python 代码。

exlink2012的专栏 412

Python装饰器深度解析:从基础到高级用法

Python装饰器深度解析:从基础到高级用法 本文深入探讨Python装饰器的核心概念和应用。主要内容包括: 装饰器基础:介绍装饰器作为高阶函数的本质,解释Python中函数作为"一等公民"的特性及闭包机制 工作原理:剖析装饰器的执行流程和等价形式 内置装饰器:详解@property、@staticmethod和@classmethod的用法 进阶应用:讲解如何处理带参数函数、保留元信息等高级技巧 文章通过丰富代码示例和理论分析,系统性地展示了装饰器在Python编程中的强大功能和优雅实

闲人编程的博客 1344

Python编程实战利用装饰器优化代码结构

装饰器是Python中一种强大的语法特性,它允许在不修改原函数代码的情况下,为函数添加额外的功能。在使用装饰器时,需要注意保留原函数的元数据,可以使用functools.wraps装饰器来保持函数名、文档字符串等信息。合理的装饰器使用能够显著提升代码的可维护性和可扩展性,是Python编程中不可或缺的高级技巧。除了函数装饰器,Python还支持类装饰器。类装饰器通过实现__call__方法,可以维护状态信息,提供更复杂的装饰逻辑。多个装饰器可以链式应用于同一个函数,每个装饰器按从下到上的顺序依次执行。

jZjUHWnv的博客 446

把装饰器作用到类和静态方法上:Python高级技巧详解

把装饰器作用到类和静态方法上:Python高级技巧详解

KE17RS的博客 725

Python-09高阶编程之闭包与装饰器

Python高阶编程:闭包与装饰器 摘要:本文系统介绍了Python中闭包与装饰器的核心概念与应用。闭包是能够记住外部作用域变量的嵌套函数,常用于实现状态保持和配置管理。装饰器则基于闭包实现,提供了一种灵活的函数/类增强机制。文章详细讲解了基本闭包实现、记忆化缓存、配置管理器等实用场景,并展示了装饰器的创建与使用技巧,包括带参数装饰器和类装饰器。通过多个代码示例,帮助读者掌握这些高阶编程技术,提升代码复用性和可维护性。

d3y1`5 81_09 669

恶补Python 装饰器的六种写法,最强干货

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 今天给大家分享一下关于装饰器的知识点,内容非常干,全程高能,认真吸收看完,一定会对装饰器有更深的理解。 Hello,装饰器 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。 它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。 装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出

pythonputao的博客 326

深入解析Python装饰器从基础到高级应用,提升代码优雅性与复用性

Python装饰器体现了修饰而非修改的编程哲学,通过将横切关注点从业务逻辑中分离,实现了代码的高内聚低耦合。从简单的函数增强到复杂的元编程,装饰器为我们提供了优雅的代码扩展方式。掌握装饰器不仅能够提升代码质量,更能帮助开发者深入理解Python的函数式编程特性和元编程能力,是通向Python高级编程的必经之路。

ea1120262413的博客 594

掌握Python装饰器:让你的代码更优雅更强大

在Python编程的世界里,装饰器(Decorator)无疑是最优雅且强大的特性之一。它不仅能让你的代码更加简洁易读,还能大幅提升代码的复用性和可维护性。无论你是Python初学者还是经验丰富的开发者,掌握装饰器都将让你的编程技能更上一层楼。

exlink2012的专栏 1072

解密 Python 装饰器:从入门到高阶实践

解密 Python 装饰器:从入门到高阶实践

prcool的博客 1268

Python 装饰器深度解析:从入门到高级用法的完整指南

装饰器是 Python 函数式编程的精髓之一,它通过高阶函数 + 闭包的组合实现了强大的行为扩展能力。掌握装饰器不仅能提升代码的复用性和可维护性,还能让你写出更具 Python 风格(Pythonic)的程序。“装饰器就是‘包装函数’的函数。从简单的@timer到复杂的@cache, 再到现代 Web 框架中的@route@api_view,装饰器无处不在。理解并熟练运用它,是你迈向高级 Python 开发者的关键一步。现在,不妨尝试为自己常用的工具函数编写几个实用的装饰器吧!

m0_75015600的博客 857
上一篇: Python实践提升-函数
下一篇: Python实践提升-面向对象编程
andyyah晓波
博客等级 码龄7年 3368粉丝 454原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值