第16章:协程

16.1 生成器如何进化成协程

协程的底层架构在“PEP 342”中定义,并在 Python 2.5(2006 年)实现了。自此之后,yield 关键字可以在表达式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的调用方可以使用 .send(...) 方法发送数据,发送的数据会成为生成器函数中 yield 表达式的值。因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。

除了 .send(value) 方法,PEP 342 还添加了 .throw(...).close() 方法:前者的作用是让调用方抛出异常,在生成器中处理;后者的作用是终止生成器。

 

16.2 用作协程的生成器的基本行为

示例 16-2-1 展示了协程的行为:

# 1. 协程使用生成器函数定义:定义体中有 yield 关键字。
def simple_coroutine():  
    print('-> start ...')
    # 2. 如果协程只需从客户那里接收数据,那么产出的值是 None——这个值是隐式指定的,因为 yield 关键字右边没有表达式。
    x = yield  
    print('-> received: ', x)


# 3. 与创建生成器的方式一样,调用函数得到生成器对象。
d = simple_coroutine()
print(d)  # <generator object simple_demo at 0x10141da20>

# 4. 首先要调用 next(...) 函数,因为生成器还没启动,没在 yield 语句处暂停,所以一开始无法发送数据。
next(d)  

# 5. 调用这个方法后,协程定义体中的 yield 表达式会计算出 42;然后协程会恢复,一直运行到下一个 yield 表达式,或者终止。
d.send(42)  

# 6. 最后,控制权流动到协程定义体的末尾,导致生成器像往常一样抛出 StopIteration 异常。

协程可以身处四个状态中的一个,当前状态可以使用 inspect.getgeneratorstate(...) 函数确定,该函数会返回下述字符串中的一个:

  1. 'GEN_CREATED'         :等待开始执行
  2. 'GEN_RUNNING'         :解释器正在执行(只有在多线程应用中才能看到这个状态)。
  3. 'GEN_SUSPENDED'   :在 yield 表达式处暂停
  4. 'GEN_CLOSED'           :执行结束

因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才能调用 send 方法;如果协程还没激活(即状态是 'GEN_CREATED'),使用 send 方法,会出现报错:TypeError: can't send non-None value to a just-started generator。因此,始终要调用 next(my_coro) 激活协程,也可以调用 my_coro.send(None),效果一样。

最先调用 next(my_coro) 函数这一步通常称为“预激”(prime)协程 (即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。

下面举个产出多个值的例子,以便更好地理解协程的行为:

import inspect


def simple_c2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)


s2 = simple_c2(8)
print(inspect.getgeneratorstate(s2))  # GEN_CREATED(协程未启动)

# 1. 向前执行协程到第一个 yield 表达式,打印(-> Started: a = 8),然后产出 a 的值,并且暂停,等待为 b 赋值。
a = next(s2)
print(a)  # 8

print(inspect.getgeneratorstate(s2))  # GEN_SUSPENDED(协程在 yield 表达式处暂停)

# 2. 把数字 10 发给暂停的协程;计算 yield 表达式,得到 10,然后把那个数绑定给 b。
# 然后继续向下执行,打印( -> Received: b = 10),产出并返回 a + b 的值 (18),然后协程暂停,等待为 c 赋值;
x = s2.send(10)
print(x)  # 18

# 3. 把数字 88 发给暂停的协程;计算 yield 表达式,得到 88,然后把那个数绑定给 c。
# 然后继续向下执行,打印( -> Received: b = 10),然后定义体执行完毕,协程终止,生成器对象抛出 StopIteration 异常)。
y = s2.send(88)  # 此时 y 应该是为定义状态,因为生成器抛出了异常;

# 4. print(inspect.getgeneratorstate(s2))  # 可以使用命令行执行查看协程状态,现在应该是 GEN_CLOSED(协程关闭)

关键的一点是,协程在 yield 关键字所在的位置暂停执行。前面说过,在赋值语句中,= 右边的代码在赋值之前执行。因此,对于 b = yield a 这行代码来说,等到客户端代码再激活协程时才会设定 b 的值。

以上 simple_c2 协程的执行过程分为 3 个阶段:

  1. 调用 next(s2),打印第一个消息,然后执行 yield a,产出数字 8
  2. 调用 s2.send(10),把 10 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 18
  3. 调用 s2.send(88),把 88 赋值给 c,打印第三个消息,协程终止,抛出异常。

 

16.3 示例:使用协程计算移动平均值

第 7 章讨论闭包时,我们分析了如何使用对象计算移动平均值,当时定义的是一个高阶函数,用于生成一个闭包,在多次调用之间跟踪 total 和 count 变量的值,现在我们来使用协程再实现一次。

示例 16-3-1 定义一个计算移动平均值的协程

def averager():
    total = 0.0
    count = 0
    average = None
    # 这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。
    # 仅当调用方在协程上调用 .close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
    while True:
        # 这里的 yield 表达式用于暂停执行协程,把结果发给调用方;
        # 还用于接收调用方后面发给协程的值,恢复无限循环。
        term = yield average
        total += term
        count += 1
        average = total / count


# 创建协程对象
g = averager()

# 预激协程,使协程进入可用状态,执行到第一次 yield 暂停,返回 yield 右侧表达式的值;
a = next(g)
print(a)  # None

# 发送数据给协程,局部变量 term 接收,执行 yield 后续代码,并更新 total、count 和 average 的值,
# 通过 while 再次循环到 yield 暂停,再返回 yield 右侧表达式的值;
b = g.send(3)
print(b)  # 3.0

# 以此类推...
c = g.send(6)
print(c)  # 4.5
d = g.send(9)
print(d)  # 6.0

细心的读者可能迫切地想知道如何终止执行 averager 实例,因为定义体中有个无限循环。讨论如何终止协程之前,我们要先谈谈如何启动协程。使用协程之前必须“预激”,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器,接下来介绍这样一个装饰器。

 

16.4 预激协程的装饰器

如果不预激,那么协程没什么用。调用 my_coro.send(x) 之前,记住一定要先调用 next(my_coro)。为了简化协程的用法,有时会使用一个预激装饰器。

示例 16-4-1 中的是预激装饰器 coroutine

from functools import wraps


def coroutine(func):
    """装饰器:预激‘func’"""
    
    
    @wraps(func)
    def primer(*args, **kwargs):  
        # 1. 调用被装饰的函数,获取生成器对象
        gen = func(*args, **kwargs)  
        # 2. 预激生成器
        next(gen)
        # 3. 返回生成器
        return gen

    return primer


@coroutine
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count


g1 = averager()

# 再次调用 send 方法不用预激了
print(g1.send(3))  # 3.0
print(g1.send(6))  # 4.5
print(g1.send(9))  # 6.0

除此之外,使用 yield from 句法调用协程时,会自动预激,因此与示例中的 @coroutine 等装饰器不兼容。Python 3.4 标准库里的 asyncio.coroutine 装饰器(第 18 章介绍)不会预激协程,因此能兼容 yield from 句法。

 

16.5 终止协程和异常处理

协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的对象)。

下面使用 16.4 章节的示例代码来举例说明如何处理异常和终止协程;

示例 16-5-1  未处理的异常会导致协程终止:

# 导入上章节的示例
from xxx import averager


g2 = averager()  # 创建协程对象

print(g2.send(30))  # 30

# 发送的值不是数字,不能加到 total 变量上,导致协程内部有异常抛出。
print(g2.send('60'))  # TypeError: unsupported operand type(s) for +=: 'float' and 'str'

# 由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出 StopIteration 异常。
print(g2.send(90))  

上方示例暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 NoneEllipsis 等常量也经常用作哨符值。Ellipsis 的优点是,数据流中不太常有这个值。我还见过有人把 StopIteration 类(类本身,不是实例,也不抛出)作为哨符 my_coro.send(StopIteration)

从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。这两个方法是 throwclose

  1. generator.throw(exc_type[, exc_value[, traceback]])

    致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产 出的值会成为调用 generator.throw 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

  2. generator.close()

    致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。 如果生成器没有处理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出 RuntimeError 异常。 生成器抛出的其他异常会向上冒泡,传给调用方。

下面举例说明如何使用 closethrow 方法控制协程:

示例 16-5-2  学习在协程中处理异常的测试代码:

class DemoException(Exception):
    """为这次演示定义的异常类型。"""


def demo_exc_handling():
    print('-> coroutine started')

    while True:
        try:
            x = yield
        # 1. 特别处理 DemoException 异常。
        except DemoException:
            print('*** DemoException handled. Continuing...')
        # 2. 如果没有异常,那么显示接收到的值。
        else:
            print('-> coroutine received: {!r}'.format(x))

    # 此行代码永远无法访问到,因为只有未处理的异常才会中止那个无限循环,而一旦出现未处理的异常,协程会立即终止。
    raise RuntimeError('This line should never run.')

示例 16-5-3 激活和关闭 demo_exc_handling,没有异常:

# 创建协程对象
exc_coro = demo_exc_handling()

next(exc_coro)  # 激活协程
# -> coroutine started

exc_coro.send(11)
# -> coroutine received: 11

exc_coro.send(22)
# -> coroutine received: 22

exc_coro.close()  # 关闭协程

print(inspect.getgeneratorstate(exc_coro))  # GEN_CLOSED

示例 16-5-4 把 DemoException 异常传入 demo_exc_handling 不会导致协程中止:

exc_coro = demo_exc_handling()

next(exc_coro)  # 激活协程
# -> coroutine started

exc_coro.send(11)
# coroutine received: 11

exc_coro.throw(DemoException)
# *** DemoException handled. Continuing...

inspect.getgeneratorstate(exc_coro)  # GEN_SUSPENDED(协程暂停)

示例 16-5-5 如果无法处理传入的异常,协程会终止 :

exc_coro = demo_exc_handling()

next(exc_coro)  # 激活协程
# -> coroutine started

exc_coro.send(11)
# coroutine received: 11

exc_coro.throw(ZeroDivisionError)
# Traceback (most recent call last):
# ...
# ZeroDivisionError

inspect.getgeneratorstate(exc_coro)  # GEN_CLOSED(协程关闭)

示例 16-5-6 如果不管协程如何结束都想做些清理工作,要把协程定义体中相关的代码放入 try/finally 块中:

def demo_exc_handling():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Continuing...')
            else:
                print('-> coroutine received: {!r}'.format(x))
                
    finally:  # 不管协程如何结束,都会执行此处代码,可以做些清理工作
        print('-> coroutine ending')

 

16.6 让协程返回值

Python 3.3 引入 yield from 结构的主要原因之一与把异常传入嵌套的协程有关,另一个原因是让协程更方便地返回值。

示例 16-6-1 是 一个新版本的 averager 协程,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么做是为了强调某些协程不会产出值,而是在最后返回一个值(通常是某种累计值)。

示例 16-6-1 定义一个求平均值的协程,让它返回一个结果

from collections import namedtuple

Result = namedtuple('Result', 'count average')


def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        # 为了返回值,协程必须正常终止;因此,这一版 averager 中有个条件判断,以便退出累计循环。
        if term is None:  
            break
        total += term
        count += 1
        average = total / count
    
    # 返回一个 namedtuple,包含 count 和 average 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。
    return Result(count, average)  


coro_avg = averager()

next(coro_avg)

coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(50)

# 发送 None 会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出 StopIteration 异常。
# 异常对象的 value 属性保存着返回的值。
try:
    coro_avg.send(None)
except StopIteration as e:
    result = e.value

print(result)  # Result(count=3, average=30.0)
print(type(result))  # <class '__main__.Result'>

注意,return 表达式的值会偷偷传给调用方,赋值给 StopIteration 异常的一个属性。这样做有点不合常理,但是能保留生成器对象的常规 行为——耗尽时抛出 StopIteration 异常。

获取协程的返回值虽然要绕个圈子,但这是 PEP 380 定义的方式,当我们意识到这一点之后就说得通了:yield from 结构会在内部自动捕获 StopIteration 异常,这种处理方式与 for 循环处理 StopIteration 异常的方式一样。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性的值变成 yield from 表达式的值。

下面会举例说明如何使用 yield from 结构按照 PEP 380 定义的方式获取 averager 协程返回的值。

 

16.7 使用 yield from

首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为继续使用那个关键字多少会引起误解。在其他语言 中,类似的结构使用 await 关键字,这个名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen() 时,subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传 入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

若想使用 yield from 结构,就要大幅改动代码。为了说明需要改动的部分,PEP 380 使用了一些专门的术语:

  • 委派生成器:包含 yield from <iterable> 表达式的生成器函数。

  • 子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器。

  • 调用方:PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。

示例 16-7-1 能很好地说明 yield from 结构的用法:

from collections import namedtuple

Result = namedtuple('Result', 'count average')


# 子生成器
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total / count
    return Result(count, average)


# 委派生成器
def grouper(results, key):
    while True:
        results[key] = yield from averager()


# 客户端代码,即调用方
def main(data_):
    results = {}
    for key, values in data_.items():
        group = grouper(results, key)
        next(group)
        for value in values:
            group.send(value)

        group.send(None)  # 中断协程

    print(results)  # 调试
    report(results)


# 输出报告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(
            result.count, group, result.average, unit))


data = {
    'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

if __name__ == '__main__':
    main(data)

    #  9 boys  averaging 40.42kg
    #  9 boys  averaging 1.39m
    # 10 girls averaging 42.04kg
    # 10 girls averaging 1.43m

图 16-7-1 把该示例中各个相关的部分标识出来了。

示例 16-7-1 中展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个 子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达式的简单生成器结束,也能以任何可迭代的对象结束。

任何 yield from 链条都必须由客户驱动,在最外层委派生成器上调用 next(...) 函数或 .send(...) 方法。可以隐式调用,例如使用 for 循环。

 

16.8 yield from 的意义

PEP 380 在“Proposal”一节分 6 点说明了 yield from 的行为:

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
  • yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。

yield from 结构的另外两个特性与异常和终止有关:

  • 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。
  • 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么就在子生成器上也调用 close() 方法,如果它有的话。如果调用子生成器的 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

假设 yield from 出现在委派生成器中。客户端代码驱动着委派生成器,而委派生成器驱动着子生成器。那么,为了简化涉及到的逻辑,我 们假设客户端没有在委派生成器上调用 .throw(...) .close() 方法。此外,我们还假设子生成器不会抛出异常,而是一直运行到终止, 让解释器抛出 StopIteration 异常。

示例 16-8-1 中的脚本就做了这些简化逻辑的假设。其实,在真实的代码 中,委派生成器应该运行到结束。下面来看一下在这个简化的美满世界中,yield from 是如何运作的。

示例 16-8-1

# 委派生成器
RESULT = yield from EXPR


# 简化的伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句(这里针对的是最简单的情况:不支持 .throw(...) 和 .close() 方法,而且只处理 StopIteration 异常)
# 用来说明 yield from 作为委派生成器的大致工作流程:

_i = iter(EXPR)  # 1. EXPR 可以是任何可迭代的对象,_i 是新的子生成器
try:
    _y = next(_i)  # 2. 预激子生成器,结果保存在 _y 中,作为产出的第一个值
except StopIteration as _e:
    _r = _e.value  # 3. 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r ——这是最简单情况下的返回值(RESULT)
else:
    while 1:  # 4. 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器 _i 之间的通道
        _s = yield _y  # 5. 产出子生成器当前产出的元素,等待调用方发送 _s 中保存的值
        try:
            _y = _i.send(_s)  # 6. 尝试让子生成器向前执行,转发调用方发送的 _s
        except StopIteration as _e:  # 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行。
            _r = _e.value
            break
RESULT = _r  # 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin 协程是一种轻量级的线程处理方式,它可以让开发者更加方便地处理异步任务。本文将讲解 Kotlin 协程的使用方法。 ## 协程的概念 协程是一种轻量级的线程处理方式,它可以在一个线程中处理多个任务。在协程中,任务可以在任何时间点被挂起并在稍后的时间点恢复执行。与线程不同,协程是由程序员控制的,因此可以更加高效地使用系统资源。 ## 协程的优点 1. 更加高效地使用系统资源:协程可以在一个线程中处理多个任务,因此可以更加高效地使用系统资源。 2. 更加方便的异步处理:协程可以让开发者更加方便地处理异步任务。在协程中,异步任务可以像同步任务一样编写,这样可以让代码更加简洁易懂。 3. 更加灵活的控制流程:协程可以让程序员更加灵活地控制流程。在协程中,任务可以在任何时间点被挂起并在稍后的时间点恢复执行,这样可以让程序员更加方便地控制任务的执行顺序。 ## 协程的使用方法 ### 1. 创建协程 在 Kotlin 中,可以使用 `launch` 函数来创建一个协程。下面是一个简单的示例: ```kotlin import kotlinx.coroutines.* fun main() { GlobalScope.launch { println("Hello, World!") } } ``` 在上面的示例中,`GlobalScope` 表示全局协程作用域,`launch` 函数表示创建一个协程。在协程中,我们可以编写需要执行的代码,这里我们只是打印了一句话。 ### 2. 挂起协程协程中,可以使用 `delay` 函数来挂起协程。下面是一个简单的示例: ```kotlin import kotlinx.coroutines.* fun main() { GlobalScope.launch { println("Start") delay(1000) println("End") } } ``` 在上面的示例中,我们使用 `delay` 函数来挂起协程,让程序等待一秒钟后再执行后面的代码。 ### 3. 异步处理 在协程中,可以使用 `async` 函数来处理异步任务。下面是一个简单的示例: ```kotlin import kotlinx.coroutines.* fun main() { val deferred = GlobalScope.async { delay(1000) "Hello, World!" } println(deferred.await()) } ``` 在上面的示例中,我们使用 `async` 函数来处理异步任务。在异步任务中,我们使用 `delay` 函数来模拟一个耗时的操作。在主线程中,我们使用 `await` 函数来等待异步任务的执行结果,并打印出来。 ## 总结 协程是一种轻量级的线程处理方式,它可以让开发者更加方便地处理异步任务。在协程中,任务可以在任何时间点被挂起并在稍后的时间点恢复执行。协程可以更加高效地使用系统资源,更加方便地异步处理,更加灵活地控制流程。在 Kotlin 中,可以使用 `launch` 函数来创建一个协程,使用 `delay` 函数来挂起协程,使用 `async` 函数来处理异步任务。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值