Python源码剖析[13] —— 字典对象PyDictObject(2)

[绝对原创 转载请注明出处]

Python源码剖析

——字典对象PyDictObject(2)

本文作者: Robert Chen (search.pythoner@gmail.com)

3         PyDictObject的创建和维护

3.1.1    PyDictObject对象创建

[dictobject.c]

typedef PyDictEntry dictentry;

typedef PyDictObject dictobject;

 

 

#define INIT_NONZERO_DICT_SLOTS(mp) do {                /

    (mp)->ma_table = (mp)->ma_smalltable;               /

    (mp)->ma_mask = PyDict_MINSIZE - 1;             /

    } while(0)

 

 

#define EMPTY_TO_MINSIZE(mp) do {                   /

    memset((mp)->ma_smalltable, 0, sizeof((mp)->ma_smalltable));    /

    (mp)->ma_used = (mp)->ma_fill = 0;              /

    INIT_NONZERO_DICT_SLOTS(mp);                    /

    } while(0)

 

 

PyObject* PyDict_New(void)

{

    register dictobject *mp;

    if (dummy == NULL) { /* Auto-initialize dummy */

        dummy = PyString_FromString("<dummy key>");

        if (dummy == NULL)

            return NULL;

    }

if (num_free_dicts)

{

        …… //使用缓冲池

}

else

{

        mp = PyObject_GC_New(dictobject, &PyDict_Type);

        if (mp == NULL)

            return NULL;

        EMPTY_TO_MINSIZE(mp);

    }

    mp->ma_lookup = lookdict_string;

    _PyObject_GC_TRACK(mp);

    return (PyObject *)mp;

}

 

 

值得注意的是,在第一次调用PyDict_New时,会创建在前面提到的那个dummy对象。显而易见,dummy对象仅仅是一个PyStringObject对象,它作为一种指示标志,表明该entry曾被使用过,且探测序列下一个位置的entry有可能是有效的,从而防止探测序列中断。

num_free_dicts可以看出,Pythondict的实现同样使用了缓冲池。我们把将缓冲池的讨论放到后边。

创建的过程首先申请合适的内存空间,然后在EMPTY_TO_MINSIZE中,会将ma_smalltable清零,同时设置ma_sizema_fill,当然,在一个PyDictObject对象刚被创建的时候,这两个变量都应该是0。然后会将ma_table指向ma_smalltable,并设置ma_mask,可以看到,ma_mask确实与一个PyDictObject对象中entry的数量有关。在创建过程的最后,将lookdict_string赋给了ma_lookup。正是ma_lookup指定了PyDictObjectentry集合中搜索某一特定entry时要进行的动作,它是PyDictObject的搜索策略,万众瞩目。

3.1.2    元素搜索

PyDictObject引入了两个不同的搜索策略,lookdictlookdict_string。实际上,这两个策略使用的是相同的算法,lookdict_string只是lookdict的一种针对PyStringObject对象的特化形式。以PyStringObject对象作为PyDictObject对象中entry的键在Python中是如此地广泛和重要,所以lookdict_string也就成为了PyDictObject创建时所默认采用的搜索策略:

[dictobject.c]

static dictentry* lookdict_string(dictobject *mp, PyObject *key, register long hash)

{

    register int i;

    register unsigned int perturb;

    register dictentry *freeslot;

    register unsigned int mask = mp->ma_mask;

    dictentry *ep0 = mp->ma_table;

    register dictentry *ep;

 

 

    if (!PyString_CheckExact(key)) {

        mp->ma_lookup = lookdict;

        return lookdict(mp, key, hash);

}

//[1]

    i = hash & mask;

ep = &ep0[i];

 

 

//[2]

//if NULL or interned

    if (ep->me_key == NULL || ep->me_key == key)

        return ep;

   

//[3]

    if (ep->me_key == dummy)

        freeslot = ep;

else

{

    //[4]

        if (ep->me_hash == hash && _PyString_Eq(ep->me_key, key))

{

            return ep;

        }

        freeslot = NULL;

    }

 

 

    /* In the loop, me_key == dummy is by far (factor of 100s) the

       least likely outcome, so test for that last. */

for (perturb = hash; ; perturb >>= PERTURB_SHIFT)

{

        i = (i << 2) + i + perturb + 1;

        ep = &ep0[i & mask];

        if (ep->me_key == NULL)

            return freeslot == NULL ? ep : freeslot;

        if (ep->me_key == key

            || (ep->me_hash == hash

                && ep->me_key != dummy

            && _PyString_Eq(ep->me_key, key)))

            return ep;

        if (ep->me_key == dummy && freeslot == NULL)

            freeslot = ep;

    }

}

 

 

其中的[1][2][3][4]标注出了搜索过程中的关键步骤,这些步骤会在后面讲述PyDictObject对象的一般搜索策略时详细讨论。

lookdict_string并不是PyDictObject中最一般的搜索策略,它是一种有条件限制的搜索策略。lookdict_string背后有一个假设,即PyDictObject对象中每一个entrykey都是PyStringObject*。只有在这种假设成立的情况下,lookdict_string才会被使用。可以看到,lookdict_string首先会检查需要搜索的key是否严格对应一个PyStringObject对象,只有在检查通过后,才会进行下面的动作;如果检查不通过,那么就会转向PyDictObject中的通用搜索策略lookdict

[dictobject.c]

static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash)

{

    register int i;

    register unsigned int perturb;

    register dictentry *freeslot;

    register unsigned int mask = mp->ma_mask;

    dictentry *ep0 = mp->ma_table;

    register dictentry *ep;

    register int restore_error;

    register int checked_error;

    register int cmp;

    PyObject *err_type, *err_value, *err_tb;

    PyObject *startkey;

 

 

    //[1]

    i = hash & mask;

ep = &ep0[i];

//[2]

    if (ep->me_key == NULL || ep->me_key == key)

        return ep;

 

 

    //[3]

    if (ep->me_key == dummy)

        freeslot = ep;

else

{

    //[4]

        if (ep->me_hash == hash)

{

            startkey = ep->me_key;

            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);

            if (cmp < 0)

                PyErr_Clear();

            if (ep0 == mp->ma_table && ep->me_key == startkey)

{

                if (cmp > 0)

                    goto Done;

            }

            else

{

                ep = lookdict(mp, key, hash);

                goto Done;

            }

        }

        freeslot = NULL;

    }

    。。。。。。

Done:

    return ep;

}

 

 

PyDictObject中维护的entry的数量是有限的,比如100个或1000个。而传入lookdict中的keyhash值却不一定会在这个范围内,所以这就要求lookdicthash值映射到某个entry上去。Lookdict采取的策略非常简单,直接将hash值与entry的数量做一个与操作,结果自然落在entry的数量之下。由于ma_mask会被用来进行大量的与操作,所以这个与entry数量相关的变量被命名为ma_mask,而不是ma_size

我们注意到,lookdict永远都不会返回NULL,如果在PyDictObject中搜索不到待查找的元素,同样会返回一个entry,这个entryme_valueNULL

在搜索的过程中freeslot是一个重要的变量。如果在探测序列中的某个位置上,entry处于Dummy态,那么如果在这个序列中搜索不成功,就会返回这个处于Dummy态的entry。我们知道,处于Dummy态的entryme_value是为NULL的,所以这个返回结果指示了搜索失败;同时,返回的entry也是一个可以立即被使用的entry,因为Dummy态的entry并没有维护一个有效的(keyvalue)对。这个freeslot正是用来指向探测序列中第一个处于Dummy态的entry,如果搜索失败,freeslot就会挺身而出,提供一个能指示失败并立即可用的entry。当然,如果探测序列中并没有Dummyentry,搜索失败时,一定是在一个处于Unused态的entry上结束搜索过程的,这时会返回这个处于Unused态的entry,同样是一个能指示失败且立即可用的entry

下面是lookdict中进行第一次检查时需要注意的动作:

[1]:根据hash值获得entry的序号。

[2]:如果ep->me_keyNULL,且与key相同,搜索失败。

[3]:若当前entry处于Dummy态,设置freeslot

[4]:检查当前Activeentry中的key与待查找的key是否相同,如果相同,则立即返回,搜索成功。

[4]中,需要注意那个PyObject_RichCompareBool,它的函数原形为:

int PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)

(v op w)成立时,返回1;当(v op w)不成立时,返回0;如果在比较中发生错误,则返回-1

现在我们考察的是根据hash值获得的第一个entry与待查找的元素的比较。实际上,由于对应于某一个散列值,几乎都有一个探测序列与之对应,所以我们现在只是考察了探测序列中第一个位置的entry。万里长征仅仅迈出了第一步。

如果第一个entry与待查找的key不匹配,那么很自然地,lookdict会沿着探测序列,顺藤摸瓜,依次比较探测序列上的entry与待查找的key

[dictobject.c]

static dictentry* lookdict(dictobject *mp, PyObject *key, register long hash)

{

    register int i;

    register unsigned int perturb;

    register dictentry *freeslot;

    register unsigned int mask = mp->ma_mask;

    dictentry *ep0 = mp->ma_table;

    register dictentry *ep;

    register int restore_error;

    register int checked_error;

    register int cmp;

    PyObject *err_type, *err_value, *err_tb;

PyObject *startkey;

。。。。。。

for (perturb = hash; ; perturb >>= PERTURB_SHIFT)

{

        //[5]

        i = (i << 2) + i + perturb + 1;

        ep = &ep0[i & mask];

       

        //[6]

        if (ep->me_key == NULL)

{

            if (freeslot != NULL)

                ep = freeslot;

            break;

        }

        if (ep->me_key == key)//[7]

            break;

        if (ep->me_hash == hash && ep->me_key != dummy)

{

            startkey = ep->me_key;

            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);

            if (cmp < 0)

                PyErr_Clear();

            if (ep0 == mp->ma_table && ep->me_key == startkey) {

                if (cmp > 0)

                    break;

            }

            else {

                ep = lookdict(mp, key, hash);

                break;

            }

        }

        //[8]

        else if (ep->me_key == dummy && freeslot == NULL)

            freeslot = ep;

    }

 

 

Done:

    return ep;

}

[5]:获得探测序列中的下一个待探测的entry

[6]ep到达一个Unusedentry,表明搜索结束。这是如果freeslot不为空,则返回freeslot所指entry

[7]entry与待查找的key匹配,搜索成功。

[8]:在探测序列中发现Dummyentry,设置freeslot

到这里,我们已经清晰地了解了PyDictObject中的搜索策略。再回过头去看看那个lookdict_string,可以很清晰地看到,lookdict_string实际上就是一个lookdict对于PyStringDict对象的优化版本。在这里展示的lookdict代码经过了删节,实际上,在lookdict中有许多捕捉错误并处理错误的代码,因为lookdict面对的是PyObject*,所以会出现很多意外情况。而在lookdict_string中,完全没有了这些处理错误的代码。而另一方面,在lookdict中,使用的是非常通用的PyObject_RichCompareBool,而lookdict_string使用的是_PyString_Eq,比要简单很多,这些因素使得lookdict_string的搜索效率要比lookdict高很多。

此外,Python自身也大量使用了PyDictObject对象,用来维护一个作用域中变量名和变量值之间的对应关系,或是用来在为函数传递参数时维护参数名与参数值的对应关系。这些对象几乎都是用PyStringObject对象作为entry中的key,所以lookdict_string的意义就显得非常重要了,它对Python整体的运行效率都有着重要的影响。

3.1.3    插入与删除

PyDictObject对象中元素的插入动作建立在搜索的基础之上:

[dictobject.c]

static void

insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value)

{

    PyObject *old_value;

    register dictentry *ep;

   

ep = mp->ma_lookup(mp, key, hash);

//[1]

    if (ep->me_value != NULL) {

        old_value = ep->me_value;

        ep->me_value = value;

        Py_DECREF(old_value); /* which **CAN** re-enter */

        Py_DECREF(key);

}

//[2]

    else {

        if (ep->me_key == NULL)

            mp->ma_fill++;

        else

            Py_DECREF(ep->me_key);

        ep->me_key = key;

        ep->me_hash = hash;

        ep->me_value = value;

        mp->ma_used++;

    }

}

 

 

前面我们提到了,搜索操作在成功时,返回相应的处于Active态的entry,而在搜索失败时会返回两种不同的结果:一是处于Unused态的entry;二是处于Dummy态的entry。那么插入操作对应不同的entry,所需要进行的动作显然也是不一样的。对于Activeentry,只需要简单地替换me_value值就可以了;而对于UnusedDummyentry,则需要完整地设置me_keyme_hashme_value

insertdict中,正是根据搜索的结果采取了不同的动作:

[1] :搜索成功,返回处于Activeentry,直接替换me_value

[2] :搜索失败,返回UnusedDummyentry,完整设置me_keyme_hashme_value

Python中,对PyDictObject中的元素进行插入或设置有两种方式:

[python code]

d = {}

d[1] = 1

d[1] = 2

 

 

第二行Python代码是在PyDictObject对象中没有这个entry的情况下插入元素,第三行是在PyDictObject对象中已经有这个entry的情况下重新设置元素。可以看到,insertdict完全可以适应这两种情况,在insertdict中,[2]处代码处理第二行Python代码,[1]处代码处理第三行Python代码。实际上,这两行Python代码也确实都调用了insertdict

当这两行设置PyDictObject对象元素的Python代码被执行时,并不是直接就调用insertdict,因为观察代码可以看到,insertdict需要一个hash值作为调用参数,那么这个hash值在什么地方获得的呢?实际上,在调用insertdict之前,还会调用PyDict_SetItem

[dictobject.c]

int PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)

{

    register dictobject *mp;

    register long hash;

    register int n_used;

 

 

mp = (dictobject *)op;

//计算hash

    if (PyString_CheckExact(key)) {

        hash = ((PyStringObject *)key)->ob_shash;

        if (hash == -1)

            hash = PyObject_Hash(key);

    }

    else {

        hash = PyObject_Hash(key);

        if (hash == -1)

            return -1;

    }

    n_used = mp->ma_used;

    Py_INCREF(value);

    Py_INCREF(key);

    insertdict(mp, key, hash, value);

   

    if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))

        return 0;

    return dictresize(mp, mp->ma_used*(mp->ma_used>50000 ? 2 : 4));

}

 

 

PyDict_SetItem中,会首先获得keyhash值,在上面的例子中,也就是一个PyIntObject对象1hash值。

然后再调用insertdict进行元素的插入或设置。

PyDict_SetItem在插入或设置元素的动作结束之后,并不会草草返回了事。接下来,它会检查是否需要改变PyDictObject内部ma_table所值的内存区域的大小,在以后的叙述中,我们将这块内存称为“table”。那么什么时候需要改变table的大小呢。在前面我们说过,如果table的装载率大于2/3时,后续的插入动作遭遇到冲突的可能性会非常大。所以装载率是否大于或等于2/3就是判断是否需要改变table大小的准则。在PyDict_SetItem中,有如下的代码:

    if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))

        return 0;

经过转换,实际上可以得到:

(mp->ma_fill)/(mp->ma_mask+1) >= 2/3

这个等式左边的表达式正是装载率。

然而装载率只是判定是否需要改变table大小的一个标准,还有另一个标准是在insertdict的过程中,是否使用了一个处于Unused态的entry。前面我们说过,在搜索过程失败并且探测序列中没有Dummy态的entry时,就会返回一个Unused态的entryinsertdict会对这个entry进行填充。只有当这种情况发生并且装载率超标时,才会进行改变table大小的动作。而判断在insertdict的过程中是否填充了Unused态的entry,是通过

mp->ma_used > n_used

来判断的,其中的n_used就是进行insertdict操作之前的mp->ma_used。通过观察mp->ma_used是否改变,就可以知道是否有Unused态的entry被填充。

在改变table时,并不一定是增加table的大小,同样也可能是减小table的大小。更改table的大小时,新的table的空间为:

mp->ma_used*(mp->ma_used>50000 ? 2 : 4)

如果一个PyDictObject对象的table中只有几个entry处于Active态,而大多数entry都处于Dummy态,那么改变table大小的结果显然就是减小了table的空间大小。

在确定新的table的大小时,通常选用的策略是时新的tableentry的数量是现在tableActiveentry数量的4倍,选用4倍是为了使table中处于Active态的entry的分布更加稀疏,减少插入元素时的冲突概率。当然,这是以内存空间为代价的。由于机器的内存总是有限的,Python总不能没心没肺地在任何时候都要求4倍空间,这样搞,别的程序会有意见的:)所以当tableActive态的entry数量非常巨大时,Python只会要求2倍的空间,这次又是以执行速度来交换内存空间。Python2.4.1将这个“非常巨大”的标准划定在50000。如此一来,上帝的归上帝,撒旦的归撒旦,万事大吉 :)

至于具体的改变table大小的重任,则交到了dictresize一人的肩上:

[dictobject.c]

static int dictresize(dictobject *mp, int minused)

{

    int newsize;

    dictentry *oldtable, *newtable, *ep;

    int i;

    int is_oldtable_malloced;

    dictentry small_copy[PyDict_MINSIZE];

//[1]

    for(newsize = PyDict_MINSIZE; newsize <= minused && newsize > 0; newsize <<= 1)

        ;

    oldtable = mp->ma_table;

    assert(oldtable != NULL);

    is_oldtable_malloced = oldtable != mp->ma_smalltable;

 

 

    //[2]

    if (newsize == PyDict_MINSIZE) {

        newtable = mp->ma_smalltable;

        if (newtable == oldtable) {

            if (mp->ma_fill == mp->ma_used) {

                //没有任何Dummyentry,直接返回

                return 0;

            }

            //oldtable拷贝,进行备份

            assert(mp->ma_fill > mp->ma_used);

            memcpy(small_copy, oldtable, sizeof(small_copy));

            oldtable = small_copy;

        }

    }

    else {

        newtable = PyMem_NEW(dictentry, newsize);

}

 

 

    //[3]

    assert(newtable != oldtable);

    mp->ma_table = newtable;

    mp->ma_mask = newsize - 1;

    memset(newtable, 0, sizeof(dictentry) * newsize);

    mp->ma_used = 0;

    i = mp->ma_fill;

    mp->ma_fill = 0;

 

 

    //[4]

    for (ep = oldtable; i > 0; ep++) {

        if (ep->me_value != NULL) { /* active entry */

            --i;

            insertdict(mp, ep->me_key, ep->me_hash, ep->me_value);

        }

        else if (ep->me_key != NULL) {  /* dummy entry */

            --i;

            assert(ep->me_key == dummy);

            Py_DECREF(ep->me_key);

        }

    }

    if (is_oldtable_malloced)

        PyMem_DEL(oldtable);

    return 0;

}

 

 

[1] dictresize首先会确定新的table的大小,很显然,这个大小一定要大于传入的参数minused,这也是在原来的table中处于Active态的entry的数量。dictresize8开始,以指数方式增加大小,直到超过了minused为止。所以实际上新的table的大小在大多数情况下至少是原来tableActiveentry数量的4倍。

[2] :如果在[1]中获得的新的table大小为8,则不需要在堆上分配空间,直接使用ma_smalltable就可以了;否则,则需要在堆上分配空间。

[3] :对新的table进行初始化,并调整原来PyDictObject对象中用于维护table使用情况的变量。

[4] :对原来table中的非Unusedentry进行处理。对于Activeentry,显然需要将其插入到新的table中,这个动作由前面考察过的insertdict完成;而对于Dummy态的entry,则略过,不做任何处理,因为我们知道Dummyentry存在的唯一理由就是为了不使搜索时的探测序列中断。现在所有Active态的entry都重新依次插入新的table中,它们会形成一条新的探测序列,不再需要这些Dummy态的entry了。

现在,利用我们对PyDictObject的认识,想象一下从table中删除一个元素应该怎样操作呢?

[dictobject.c]

int PyDict_DelItem(PyObject *op, PyObject *key)

{

    register dictobject *mp;

    register long hash;

    register dictentry *ep;

PyObject *old_value, *old_key;

//获得hash

    if (!PyString_CheckExact(key) ||

        (hash = ((PyStringObject *) key)->ob_shash) == -1) {

        hash = PyObject_Hash(key);

        if (hash == -1)

            return -1;

}

//搜索entry

    mp = (dictobject *)op;

    ep = (mp->ma_lookup)(mp, key, hash);

//删除entry所维护的元素

    old_key = ep->me_key;

    Py_INCREF(dummy);

    ep->me_key = dummy;

    old_value = ep->me_value;

    ep->me_value = NULL;

    mp->ma_used--;

    Py_DECREF(old_value);

    Py_DECREF(old_key);

    return 0;

}

 

 

流程非常清晰,先计算hash值,然后搜索相应的entry,最后删除entry中维护的元素,并将entryActive态变换为Dummy态,同时还将调整PyDictObject对象中维护table使用情况的变量。

下面我们用一个简单的例子来动态地展示对PyDictObjecttable的维护过程,需要提醒的是,这里采用的散列函数和探测函数都与PythonPyDictObject实际采用的策略不同,这里只是想要从观念上展示对table的维护过程。在下面的图中,蓝色代表Unusedentry,绿色为Active态,黄色为Dummy态。

假如table中有10entry,散列函数为HASH(x) = x mod 10,冲突解决方案采用线性探测,且探测函数为x = x + 1。假设向table中依次加入了以下元素对:(44),(1414),(2424),(3434),则加入元素后的entry为:

 

 

现在删除元素对(1414),然后向table中插入新的元素对(104104)。则在搜索的过程中,由于在原来维护14entry#4处于现在Dummy态,所以freeslots会指向这个可用的entry

搜索完成后,填充freeslot所指向的entry

然后,再向table中插入元素对(1414),这时由于探测序列上已经没有Dummyentry了,所以最后返回的ep会指向一个处于Unused态的entry

最后插入元素对(1414),结果为:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,今天我们来学习Python字典Dictionary)。 字典是一种无序的、可变的数据类型,它以键值对(key-value)的形式存储数据,其键(key)必须是唯一的,而值(value)可以是任意数据类型。字典用花括号{}表示,每个键值对之间用逗号隔开。 下面是一个简单的字典示例: ``` my_dict = {'name': '张三', 'age': 18, 'gender': '男'} ``` 这个字典,'name'、'age'、'gender'就是键,而'张三'、18、'男'就是相应的值。 可以使用`dict()`函数来创建一个字典,也可以使用`{}`来创建一个空字典。 接下来,我们来看一些常用的字典操作。 1. 访问字典元素 可以通过键来访问字典的元素,例如: ``` print(my_dict['name']) # 输出:张三 ``` 如果键不存在,则会抛出KeyError异常。 2. 修改字典元素 可以通过键来修改字典的元素,例如: ``` my_dict['age'] = 20 print(my_dict) # 输出:{'name': '张三', 'age': 20, 'gender': '男'} ``` 3. 添加字典元素 可以通过键来添加字典的元素,例如: ``` my_dict['address'] = '北京市' print(my_dict) # 输出:{'name': '张三', 'age': 20, 'gender': '男', 'address': '北京市'} ``` 4. 删除字典元素 可以通过键来删除字典的元素,例如: ``` del my_dict['gender'] print(my_dict) # 输出:{'name': '张三', 'age': 20, 'address': '北京市'} ``` 5. 字典长度 可以使用`len()`函数来获取字典的长度,例如: ``` print(len(my_dict)) # 输出:3 ``` 6. 字典遍历 可以使用`items()`方法来遍历字典的每一个键值对,例如: ``` for key, value in my_dict.items(): print(key, value) ``` 输出: ``` name 张三 age 20 address 北京市 ``` 通过上述操作,我们可以初步了解字典的基本用法。在实际应用字典是非常重要的数据类型,它可以用来存储和处理各种复杂的数据结构。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值