3.第三章 字典和集合

3. 字典和集合

说起来, Python就是包裹在一堆语法糖中的字典.
								          --Lalo Martins
								  早期数学游民, Python专家
没有Python程序不使用字典, 即使不直接出现在我们自己编写的代码中, 我们也间接用到了,
因为dict类型是实现Python的基石.
一些Python核心结构在内存中以字典的形式存在, 比如说类和实例属性, 模块命名空间, 以及函数的关键字参数.
另外, __builtins__.__dict__存储这所有内置类型, 对象和函数,

由于字典的关键作用, Python对字典做了高度优化, 而且已知在改进.
Python字典能如此高效, 要归功于'哈希表'.

处理字典之外, 内置类型中set和frozenset也基于哈希表.
这两种类型的API和运算符比其他流行语言中的集合更丰富.
具体而言, Python集合实现了集合论中的所有基本运算, 包括并集, 交集, 子集测试等.
有了这些运算, 就能以更具描述性的方式表达算法, 避免了一层层嵌套循环和测试条件.

本章涵盖了一下内容:
 构建及处理dict和映射的现代句法, 包括增加的拆包和模式匹配;
 映射类型的常用方法;
 特别处理缺失键的情况;
 标准库中的dict变体;
 set和frozenset类型;
 哈希表对几何和字典行为的影响.

3.1 本章新增内容
2版的改动大多是对映射类型新功能的介绍.
 3.2节介绍增强的拆包句法和合并映射的不同方式, 包括自Python3.9开始受dict支持的||=运算符.
 3.3节说明如何使用Python3.10引入的match/case句法处理映射.
 3.6.1节现在关注的是dict和OrderedDict之间微小却不可忽略的差异,
  毕竟自Python3.6, dict也能保留键的插入顺序.
 新增3.8节和3.12, 讲解dict.keys, dict.items, dict.values返回的视图对象.

dict和set的底层实现任然依赖于哈希表, 不过dict的代码有两项重要的优化, 节省了内存, 还保留了插入的顺序.
3.9节和3.11节综述如何合理利用这两种类型.
3.2 字典的现代句法
接下来几节介绍用于构建, 拆包和处理映射的高级语法功能.
其中一些功能在Python中早以出现, 不过你可能还不知道.
还有一些功能相对较新, 例如Python3.9引入的|运算符和Python3.10引入的match/case句法.
先从我们熟悉的功能讲起.
3.2.12 字典推导式
自Python2.7开始, 列表推导式和生成器表达式经过改造, 以适用于字典推导式(以及后文要讲的集合推导式).
'字典推导式'从任何可迭代对象中获取键值对, 构建dict实例.
示例3-1使用字典推导式根据同一个元组列表构建两个字典.
# 示例3-1 字典推导式示例

# 像dial_codes这种包含键值对的可迭代对象可以直接传递给dict构造函数, 但是......
>>> dial_codes = [
...      (880, 'Bangladesh'),
...      (55, 'Brazil'),
...      (86, 'China'),
...      (91, 'India'),
...      (62, 'Indonesia'),
...      (81, 'Japan'),
...      (234, 'Nigeria'),
...      (7, 'Russia'),
...      (1, 'United States'),
... ]

# ......这里, 我们会调了键和值的位置, 以country为键, code为值.
>>> country_dial = {country: code for code, country in dial_codes}
>>> country_dial
{'Bangladesh': 880, 'Brazil': 55, 'China': 86, 'India': 91, 'Indonesia': 62, 
'Japan': 81, 'Nigeria': 234, 'Russia': 7, 'United States': 1}

# 按国家的名称排序country_dial, 再次对调键和值, 把值转换成全大写形式, 并通过code < 70条件筛选项.
>>> {code: country.upper()
...  for country, code  in sorted(country_dial.items())
...      if code  < 70}
{55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA', 1: 'UNITED STATES'}

习惯列表推导式之后, 自然能理解字典推导式.
即使现在不理解也没关系, 推导式句法以及被人们广泛使用, 熟练掌握是迟早的事.
3.2.2 映射拆包
从Python3.5开始, 'PEP 448--Additinal Unpacking Generalizations' 在两方面增强了隐射拆包功能.
首先, 调用函数时, 不止一个参数可以使用**.
但是, 所有键都要是字符串(关键字参数), 而且在所有参数中是唯一的(因为关键字参数不可重复).
>>> def dump(**kwargs):
...		return kwargs
...
>>> dump(**{'x': 1}, y=2, **{'z': 3})
{'x': 1, 'y': 2, 'z': 3}

其次, **可以在dict字面量中使用, 同样可以多次使用.
>>> {'a': 0, **{'x': 1}, 'y': 2, **{'z': 3, 'x': 4}}
{'a': 0, 'x': 4, 'y': 2, 'z': 3}

这种情况下允许键重复, 后面的键覆盖前面的键, 比如本例中x映射的值.
这种句法也可以用于合并映射, 但是合并映射还有其他方式. 请继续往下读.
3.2.3 使用|合并映射
Python3.9支持使用||=合并映射.
这不难理解, 因为二者也是并集运算符.
|运算符创建一个新映射.
>>> d1 = {'a': 1, 'b': 3}
>>> d2 = {'a': 2, 'b': 4, 'c': 6}
>>> d1 | d2
{'a': 2, 'b': 4, 'c': 6}

通常, 新映射的类型与左操作数(本例中的d1)的类型相同.
不过, 涉及用户定义的类型是, 也可能与第二个操作数的类型相同.
这背后涉及运算符重载规则, 详见第16.
如果想就地更新现有映射, 则使用|=. 
续前例, 当时d1没有变化, 但是现在变了.
>>> d1
{'a': 1, 'b': 3}
>>> d1 |= d2
>>> d1
{'a': 2, 'b': 4, 'c': 6}

*--------------------------------------------------------------------------------------------*
如果你想要维护使用Python3.8或更早版本编写的代码, 
请看'PEP 584-Add Union Operators To dict'中的'Motivation'一节, 那里简单总结了合并映射的其他方式.
*--------------------------------------------------------------------------------------------*
下面来看如何使用模式匹配处理映射.
3.3 使用模式匹配处理映射
match/case语法的匹配对象可以是映射.
映射的模式看似dict字面量, 其实能够匹配collections.abs.Mapping的任何具体子类或虚拟子类. 
(1: 虚拟子类是调用抽象类的.register()方法注册的类, 详见13.5.6.
通过Python或C语言API实现的类型, 如果设定了Py_TPFLAGS_MAPPING标记位, 那么也能匹配.)

第二章只重点讲了序列模式, 其实不同类型的模式可以组成嵌套.
模式匹配是一种强大的工具, 借助析构可以处理嵌套的映射和序列等结构化记录.
我们经常需要从JSON API和具有半结构化模式的数据库(例如MongoDB, EdgeDB或PostgresSQL)
中读取这类记录, 示例3-2就是一例.
get_creators函数有一些简单的类型注解, 作用是明确表明参数为一个dict, 返回值是一个list.
# 示例3-2 creator.py: get_creators()函数从出版物记录中提取创作者的名字
def get_creators(record: dict) -> list:
    match record:
        # 匹配含有'type': 'book', 'api': 2, 而且'authors'键映射一个序列的映射对象. 
        # 以列表形式返回序列中的项.
        case {'type': 'book', 'api': 2, 'authors': [*names]}:
            return names

        # 匹配含有'type': 'book', 'api': 1, 而且'author'键映射任何对象的映射对象.
        # 以列表的形式返回匹配的对象.
        case {'type': 'book', 'api': 1, 'author': name}:
            return [name]

        # 其他含有'type': 'book', 的映射均无效, 抛出ValueError.
        case {'type': 'book'}:
            raise ValueError(f"Invalid 'book' record: {record!r}")

        # 匹配含有'type': 'movie', 而且'director'映射单个对象的映射对象.
        # 以列表形式返回匹配的对象.

        case {'type': 'movie', 'director': name}:
            return [name]

        # 其他匹配对象均无效, 抛出ValueError.
        case _:
            raise ValueError(f'Invalid record: {record!r}')
            
通过示例3-2可以看出处理半结构化数据(例如JSON记录)的几点注意事项:
 包含一个描述记录种类的字段(例如'type': 'movie');
 包含一个标识模式版本的字段(例如'api': 2), 方便公开API版本更迭;
 包含处理特定无效记录(例如'book')的case子句, 以及兜底case子句.
下面在doctest中测试一下get_creators函数.
>>> b1 = dict(api=1, author='Douglas Hofstadter',
...			type='book', title='Godel, Escher, Bach')
>>> get_creators(b1)
['Douglas Hofstadter']

# 导入有序字典
>>> from collections import OrderedDict
>>> b2 = OrderedDict(api=2, type='book',
... 		title='Python in a Nutshell',
...			authors='Martell Ravenscroft Holden'.split())
...
>>> get_creators(b2)
['Martell', 'Ravenscroft', 'Holden']

>>> get_creators({'type': 'book', 'pages': 770})
Traceback (most recent call last):
    ...
ValueError: Invalid 'book' record: {'type': 'book', 'pages': 770}

>>> get_creators('Spam, spam, spam')
Traceback (most recent call last):
    ...
ValueError: Invalid record: 'Spam, spam, spam'

注意, 模式中键的顺序无关紧要.
即使b2是一个OrderedDict, 也能作为匹配对象.
与序列模式不同, 就算只有部分匹配, 映射模式也算成功匹配. 
在上述doctest中, b1和b2两个匹配对象中都有'title', 尽管任何'book'模式中都没有这个键, 但依然可以匹配.
没有必要使用**extra匹配多出来的键值对, 倘若你想把多出的键值对捕获到一个dict中,
可以在一个变量前面加上**, 不过必须放在模式最后. **_是无效的, 纯属于画蛇添足. 
(会直接报错: SyntaxError: invalid syntax, 语法错误: 无效的语法.)
下面是一个简答的例子.
>>> food = dict(category='ice cream', flavor='vanilla', cost=199)
>>> match food:
... 	case{'category': 'ice cream', **details}:
...			print(f'Ice cream details: {details}')
...
Ice cream details: {'flavor': 'vanilla', 'cost': 199}

3.5节讲解的defaultdict等映射, 通过__getitem__查找键(即d[key])始终成功.
这是因为倘若缺少一项, 则自动创建.
对模式匹配而言, 仅当匹配对象在运行match语句之前以及含有所需要的键才能成功匹配.
*--------------------------------------------------------------------------------------------*
模式匹配不会自动处理缺失的键, 因为模式匹配始终使用d.get(key, sentinel)方法.
其中, sentinel是特殊的标记值, 不会出现在用户数据中.
*--------------------------------------------------------------------------------------------*
讲完句法和结构之后, 接下来研究映射API.
3.4 映射类型的标准API
collections.abc模块中的抽象基类和Mapping和MutableMapping描述dict和类似类型的接口, 见图3-1.

这两个抽象基类的主要作用是确立映射对象的标准接口, 并在需要广义上的映射对象时为isinstance提供测试标准.
>>> from collections import abc
>>> my_dict = {}
>>> isinstance(my_dict, abc.Mapping)
True
>>> isinstance(my_dict, abc.MutableMapping)
True

*---------------------------------------------提示-------------------------------------------*
使用isinstance测试是否满足抽象基类的接口要求, 往往比检查一个函数的参数是不是具体的dict类型要好,
因为除了dict之外还有其他映射类型可用.
这个问题将在第13章详谈.
*--------------------------------------------------------------------------------------------*

图3-1

3-1: MutableMapping及其collections.abc中的超类的简化UML类图.
如果想自定义映射类型, 拓展collections.UserDict或通过组合模式包装dict更简单,
那就不要定义这些抽象基类的子类.
collections.UserDict类和标准库中的所有具体映射类都在实现中封装了基本的dict, 
而dict又建立在哈希表之上.
因此, 这些类有一个共同的限制, 即键必须'可哈希'(值不要求可哈希, 只有键需要).
3.4.1节会讲一讲相关概念, 帮你回顾一下.
3.4.1 "可哈希"指什么
"可哈希"在Python术语表中有定义, 摘录(有部分改动)如下:
如果一个对象的哈希码在整个生命周期内永不改变(依托__hash__()方法),
而且可与其他对象比较(依托__eq__()方法), 那么这个对象就是可哈希的.
两个可哈希对象仅当哈希码相同时相等. 
(2: Python术语表中的'和哈希'词条使用的是'哈希值', 而不是'哈希码'.
我倾向于使用'哈希码', 因为这个概念出现在映射上下文中, 而映射对象中的项由键和值构成, 
使用'哈希值'容易与键对应的'值'混淆. 本书始终使用'哈希码'一说.)
数值类型以及不可变的扁平类型str和bytes均是可哈希的.
如果容器类型是不可变的, 而且所含的对象是可哈希的, 那么容器类型自身也是可哈希的.
frozenset对象全部是可哈希的, 因为按照定义, 每一个元素都必须是可哈希的.
仅当所有项均可哈希, tuple对象才是可哈希的.
请考察一下示例中的tt, tl和tf.
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
80271212646858338501

>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

一个对象的哈希码根据所用的Python版本和设备架构有所不同.
如果出于安全考量而在哈希计算过程中'加盐', 那么哈希码也会发生变化. 
 正确实现的对象, 其哈希码在一个Python进程内保持不变.
(3: 一些安全隐患和应对方案参见 'PEP 456 --Seccure and interchangeable hash algorithm'.)

默认情况下, 用户定义的类型是可哈希的, 因为自定义类型的哈希码取自id(),
而且继承自object类的__eq__()方法只不过比较对象ID.
如果自己实现了__eq__()方法, 根据对象的内部状态进行比较,
那么仅当__hash__()方法始终返回同一个哈希码时, 对象才是可哈希的.
实践中, 这要求__eq__()和__hash__()只考虑在对象的生命周期内始终不变的实例属性.
解释:
在Python中, 如果一个对象是可哈希的, 则它可以用作字典的键或集合的成员.
可哈希的对象必须满足以下两个条件: 

1. 对象的哈希码必须始终保持不变.
2. 对象可以使用__eq__()方法进行比较, 即如果a == b为真, 则hash(a) == hash(b)也为真.

对于自定义类型, 默认情况下Python使用该对象的id()来生成哈希码.
这意味着如果对象的内部状态发生变化, 则哈希码也会发生变化, 因此对象将不再可哈希.

但是, 如果你定义了自己的__hash__()方法, 那么你可以在该方法中根据对象的不变属性生成哈希码.
如果你确保__hash__()方法始终返回相同的哈希码, 则对象将保持可哈希.
请注意, 这意味着你必须确保在对象的生命周期中不变的属性, 否则哈希码会发生变化, 导致对象不再可哈希.

另外, 请注意, 如果你自定义了__eq__()方法, 则也需要确保它与__hash__()方法一致.
也就是说, 如果a == b为真, 则hash(a) == hash(b)也应该为真.
否则, 如果你使用自定义类型作为字典的键或集合的成员, 则可能会出现奇怪的行为.
接下来概览Python中最常用的几种映射类型的API, 包括dict, defaultdict和OrderDict.
3.4.2 常用映射方法概述
映射的基本API非常丰富.
3-1列出了dict, 以及collections模块中两个常用变体defaultdict和OrderDict实现的方法.

3-1: 映射类型dict, collections.defaultdict和collections.OrderDict实现的方法
(简单起见, 省略了object实现的方法, [...]表示可选参数)
方法                   dict  defaultdicr  orderdDict  含义
d.clear()                                      删除所有项
d.__contains__(k)                              k in d
d.copy()                                       浅拷贝
d.__copy__()                                     为copy.copy(d)提供支持
d.default_factory                                由__missing__调用的可调用对象,用于设置缺失的值(a) d.__delitem__(k)                               del d[k] : 删除键k对应的项
d.fromkeys(it, [initial])                      根据可迭代对象中的键构建一个映射, 
                                                  可选参数initial指定初始化值(默认为None)   d.get(k, [default])                            获取键k对应的项, 不存在时则返回default或None
d.__getitem__(k)                               d[k] : 获取键k对应的项
d.items()                                      获取项视图, (key, value)
d.__iter__()                                   获取遍历见的迭代器
d.keys()                                       获取键视图
d.__len__()                                    len(d) : 项数
d.__missing__(k)                               当__getitem__找不到键是调用
d.move_to_end(k, [last])                         把k移到开头或结尾(last默认为True)
d.__or__(other)                                d1 | d2 : 合并d1和d2, 新建一个dict对象
                                                  (Python3.9及以上版本)
d.__ior__(other)                               d1 |= d2 : 使用d2中的项更新d1 
                                                  (Python3.9及以上版本)
d.pop(k, [default])                            删除并返回k对应的项, 
                                                  如果没有键k, 则返回default或None
d.popitem()                                    删除并返回((key, value)形式)最后插入的项(b)
d.__reversed__()                               reverse(d): 按插入顺序从后向前返回遍历键的迭代器
d.__ror__(other)                               other | dd: 反向合并运算符(Python3.9及以上版本c)
d.setdefault(k, [default])                     如果d中有键k, 则返回d[k]; 
                                                  否则, 把d[k]设置为的fault, 并返回default
d.__setitem__(k, v)                            d[k] = v : 把键k对应的值设为v
d.update(m, [**kwargs])                        使用映射或可迭代对象中的键值更新d
d.value()                                      获取值视图

a default_factory不是方法, 而是可调用的属性, 有终端用户在实例化defaultdict对象时设置.
b OrderedDict.popitem(last=False)删除最想插入的项(即先进先出). 截止Python3.10b3, 
  dict和default不支持关键字参数last.
c 反向运算符详见第16.
d.update(m)处理第一个参数m的方式是'鸭子类型'的经典应用: 
先检查m有没有keys方法, 如果有, 则假定m是映射; 
否则, 退而求其次, 迭代m, 假定项是键值对形式((key, value)).
多数Python映射的结构函数, 内部编辑逻辑与update()方法一样, 
也就是说, 可以从其他映射或生成键值对的可迭代对象初始化(初始化映射).

setdefault()方法不容忽视.
如果想就地更新一个项的值, 使用该方法就不用再去查找键.
3.4.3节会说明具体用法.
3.4.3 插入或更新可变的值
根据Python的'快速失败'原则, 当键k不存在时, d[k]抛出错误.
深谙Python的人知道, 如果觉得默认值不抛出KetError更好, 那么可以把d[k]换成d.get(k, default).
然而, 如果你想要更新得到的可变值, 那么还有更好的方法.

举个例子.
鸡舍你想编写一个脚本分析文本, 编制索引, 生成一个映射, 以词为键, 值为一个列表, 表示词出现的位置.
如示例3-3所示.
# zen.txt
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

# 示例3-3 使用示例3-4处理<<Python之禅>>得到的部分输出, 每一行对应一个词及其出现的位置列表
# (第一个数是行号, 第二个数是序列)
$ python index0.py zen.txt
a [(19, 48), (20, 53)]
Although [(11, 1), (16, 1), (18, 1)]
ambiguity [(14, 16)]
and [(15, 23)]
are [(21, 12)]
aren [(10, 15)]
at [(16, 38)]
bad [(19, 50)]
be [(15, 14), (16, 27), (20, 50)]
beats [(11, 23)]
Beautiful [(3, 1)]
better [(3, 14), (4, 13), (5, 11), (6, 12), (7, 9), (8, 11), (17, 8), (18, 25)]
...
示例3-4种的脚本不太完美, 目的告诉你dict.get不是处理缺失键的最近佳方式.
这个脚本根据Alex Maartelli的一个示例修改而来. 
(4: 原脚本出自Martelli的演讲'Re-leaming Python', 41张幻灯片.
它的脚本其实是演示dict.setdefault, 如示例3-5所示.)
# 示例3-4 index0.py: 使用dict.get获取并更新词出现的位置列表, 编制索引(更好的方案见示例3-5)
"""构建一个索引映射, 列出词出现的位置"""
import re
import sys

# 正则表达式
WORD_RE = re.compile(r'\w+')

# 索引字典
index = {}

# sys.argv[1] 获取命令的第二个参数zen.txt
with open(sys.argv[1], encoding='utf-8') as fp:
    # 枚举文件对象, line_no为行号(共21行), line为文件内容.
    for line_no, line in enumerate(fp, 1):
        # 正则表达式查找单词, 得到一个可迭代正则对象, 最后遍历正则对象.
        for match in WORD_RE.finditer(line):
            # 通过group方法找到正则匹配到的单词.
            word = match.group()
            # 列, match.start()获取单词的起始位置(一个符号占一列), 起始位置从0开始, 所以都加1.
            column_no = match.start() + 1
            # 单词的位置(行, 列), 编制索引来记录其中各个单词出现的位置.
            location = (line_no, column_no)
            
            # 这些写不完美, 仅做演示
            # 获取word出现的位置列表, 如未找到, 则返回[].
            occurrences = index.get(word, [])
            # 把新找到的位置追加到occurrences中.
            occurrences.append(location)
            # 把更新后的occurrences放入index字典中. 这里温涵一次对index的搜索操作.
            index[word] = occurrences


# 按字母表顺序显示
# sort函数的key参数不是调用str.upper方法, 
# 而是传入那个方法的引用, 供sorted函数排序各个词, 规范化输出. ⑤
# (注5: 这里把方法当作一等对象使用, 详见第7章.)
for word in sorted(index, key=str.upper):
    print(word, index[word])

示例3-4处理occurrences用了3行代码, 换成dict.setdefault则只需要一行代码.
示例3-5与Alex Martelli最初写的代码类似.
# 示例3-5 index.py: 使用dict.setdefault获取并更新词出现的位置列表, 编制索引.
# 与示例3-4相比, 这部分只用了一行代码.
import re 
import sys 

WORD_RE = re.compile(r'\w+')

index = {}
with open(sys.argv[1], encoding='utf-8') as fp:
    for  line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)
            # 获取word出现的位置列表, 如未找到, 则设为为[];
            # setdefault返回该列表, 可以直接更新, 不用再搜索一次.
            index.setdefault(word, []).append(location)
            
            
# 按字母表顺序显示        
for word in sorted(index, key=str.upper):
    print(word, index[word])
    
也就是说, 下面这一行:
my_dict.setdefault(key, []).append(new_value)

作用与下面3行一样:
if key not in my_dict:
	my_dict[key] = []
my_dict[key].append(new_value)

不过, 后一种写法至少要搜索key两次, 如未找到则搜索3, 而setdefault只搜索一次.

3.5节将探讨一个相关话题: 处理任何操作(而不仅限于插入)缺失键的情况.
3.5 自动处理缺失的键
有时搜索的键不一定存在, 为了以防万一, 可以人为设置一个值, 以方便某些情况的处理.
人为设置的值主要有两种方法:
第一种是把普通的dict换成defaultdict;
第二种是定义dict或其他映射类型的子类, 现实__missing__方法.
下面分别介绍这两种情况.
3.5.1 defaultdict: 处理缺失键的另一种选择
对于collections.defaultdict, d[k]句法找不到搜索的键时, 使用指定的默认值创建对应的项.
示例3-6使用defaultdict重新实现示例3-5, 以另一种优雅的方式编制索引.

这词实现的原理是, 实例化defaultdict对象时提供一个可调用对象, 当__getitem__遇到键不存在的键时,
调用那个可调用对象生成一个默认值.

举个例子.
假设使用dd = defaultdict(list)创建一个defaultdict对象, 
而dd中没有'new-key', 那么dd['new-key']表达式按一下几步处理.
1. 调用list()创建一个新列表.
2. 把该列表插入dd, 对应到'new-key'键上.
3. 返回该列表的应用.

生成默认值的可调用对象存放在实例属性default_factory中.
# 示例3-6 index_default.py: 使用defaultdict代替setdefault方法
"""构建一个索引映射, 列出词出现的位置"""

import collections
import re
import sys

WORD_RE = re.compile(r'\w+')

# 创建一个defaultdict对象, 把default_factory设置为list构造函数.
index = collections.defaultdict(list)

with open(sys.argv[1], encoding='utf-8') as fp:
    for line_no, line in enumerate(fp, 1):
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)
            # 如果index中当前没有word键, 调用default_factory生成缺失的值.
            # 这里生成一个空列表, 赋值给index[word], 然后返回空列表.
            # 因此, .append(location)操作始终成功.
            index[word].append(location)
  

# 按字母表顺序显示        
for word in sorted(index, key=str.upper):
    print(word, index[word])
    
如果未提供default_factory, 遇到缺失的键时, 则像往常一样抛出异常KeyError.
***----------------------------------------------------------------------------------------***
defaultdict的default_factory仅为__getitem__提供默认值, 其他方法用不到.
例如, dd是一个defaultdict对象, 如果没有键k, 那么dd[k]将调用的default_factory创建默认值,
但是dd.get(k)依然返回Nont, 而且k in dd 也返回False.
***----------------------------------------------------------------------------------------***
defaultdict之所以调用default_factory, 背后的机制是接下来要讨论的特殊方法__missing__.
3.5.2 __missing__方法
映射处理缺失键的底层逻辑在__missing__方法中.
dict基类本身没有定义这个方法, 但是如果dict的子类定义了这个方法, 
那么dict.__getitem__找不到键是将调用__missing__方法, 不抛出KeyError.

假设你希望查找映射的键时把键转换成str类型. IoT设备的代码库就需要这么做. 
(6: 例如开发已经停滞的Pingo.io库)
可编程板(例如, Raspberry Pi或Arduino)上有一些通用I/O引脚, 
在代码库中通过Board类的my_board.pins属性表示.
这个属性把物理引脚标识符映射到引脚对象上.
物理引脚标识符可以使纯数值, 也可以是'A0''P9_12'之类的字符串.
为了保持一致, 我们希望pins中所有的键都是字符串, 可是有些时候也想通过数值来查找引脚,
例如, my_arduino.pin[13], 这是为了避免初学者不知道如何点亮arduino板13号引脚上的LED.
这种映射要求如示例3-7所示.
# 示例3-7 搜索非字符串键时, StrKeyDict0把为找打的键转换成字符串.
# 测试使用`d[key]`表示法检索项::

>>> d = StrKeyDict0([('2', 'two'), ('4', 'four')])
>>> d['2']
'two'

>>> d[4]
'four'

>>> d[1]
Traceback (most recent call last):
    ...
KeyError: '1'

# 测试使用`d.get(key)`表示法检索项::

>>> d.get('2')
'two'
>>> d.get(4)
'four'
>>> d.get(1, 'N/A')
'N/A'

# 测试`in`运算符::
>>> 2 in d
True
>>> 1 in d
False

示例3-8实现了可通过以上的doctest的StrKeyDict0类.
*--------------------------------------------------------------------------------------------*
用户自定义映射类型最好像示例3-9那样继承collections.UserDict类, 而不要继承dict类.
这里继承dict类时为了说明内置的dict.__getitem__方法支持__missing__.
*--------------------------------------------------------------------------------------------*
# 示例3-8 StrKeyDict0 在查找键时把非字符串键转换成字符串(见示例3-7中的测试)
# StrKeyDict0继承dict.
class StrKerDict0(dict):
    
    def __missing__(self, key):
        # 检查key是不是str类型. 如果是, 并且找不到这个key, 则抛出KeyError.
        if isinstance(key, str):
            raise KeyError(key)
        # 把key转换成str类型, 查找字符串键.
        return self[str(key)]
  	
    def get(self, key, default=None):
        try:
             # get方法委托__getitem__, 通过self[key]表示法查找键, 让__missing__发挥作用.
            return self[key]
        except KeyError:
            # 如果抛出KeyError, 说明__missing__也找不到键, 那就返回default.
            return default
    def __contatins__(self, key):
        # 先搜索未经过修改的键(实例可能有非字符串键), 在搜索键的字符串形式.
        return key in self.keys() or str(key) in self.keys()
    
请想一想, 为什么实现__missing__方法时要做isinstance(key, str)测试.

如果不做这个测试, 则__missing__方法可以处理任何类型的键, 是不是字符串都无所谓,
但前提是str(k)得到的是现有的键.
然而, 倘如str(k)得到的键不存在, 那就进入了无限递归: 
__missing__方法的最后一行self[str(key)]将调用__getitem__方法, 转而又调用__missing__方法.

本例中, 出于对行为一致性的考虑, 也需要__contains__方法, 
应为k in d操作调用它, 而从dict继承的__contains__方法不回落到__missing__方法.
__contains__方法的实现方式有一个微妙的细节, 我们没有按照常规的Python风格检查键(k in my_dict),
因为str(key) in self将递归调用__contains__方法.
为了避免这个问题, 我们显示地在self.keys()中查找键.
行为的一致性: 是指在不同情况下, 一个对象或方法的行为应该保持相同,
不会因为上下文的不同而导致行为不一致或意外的结果.
在这个示例中, 如果没有实现__contains__方法, 
那么使用in操作符检查一个非字符串键是否存在于StrKeyDict0实例中时,
会根据dict类的默认行为, 直接返回False, 而不会调用__missing__方法.
因此, 为了保持StrKeyDict0实例的行为一致性, 需要实现__contains__方法,
让它先检查经过修改的键(让它先检查经过修改的键), 
然后再检查键的字符串形式(直接存储在字典中的的字符串键).
这样就可以保证无论传入什么类型的键, 都可以得到一致的结果.

"回落": 指的是在一个对象或方法的某个操作没有得到正确结果时,
继续尝试其他操作或方法, 以获得正确结果的过程
在Python3中, 即使是特别大型的映射, k in my_dict.keys()这样的搜索效率也不低(意思说还是很高的),
应为dict.keys()返回的是类似集合的视图, 详见3.12.
尽管如此, k in my_dict的作用是相同的, 而且速度更快, 因为无须通过属性查找.keys方法.

示例3-8, __contains__方法使用self.keys()还有一个特定的原因.
为了保证结果正确, 必须使用key in self.keys()检查未经过修改的键, 
因为StrKeyDict0不强制要求字典中所有键都是str类型.
这个简单的示例只有一个目的, 即提高搜索的'友好度', 而不是限定键的类型.
***----------------------------------------------------------------------------------------***
用户自己定义的类, 如果继承标准库中的映射, 
在实现__getitem__, get或__contains__方法时不一定回落到_missing__方法, 具体原因见3.5.3.
***----------------------------------------------------------------------------------------***
3.5.3 标准库对__missing__方法的使用不一致
下面分几种情况演示查找缺失键的不同行为.

dict子类
定义一个dict子类, 只实现__missing__方法, 其他方法均不实现.
这种情况下, 可能只有d[k](使用继承自dict的__getitem__方法)会调用__missing__方法.

cillections.UserDict子类
定义一个UserDict子类, 同样只实现__missing__方法, 其他方法均不实现.
继承自UserDict的get方法调用__getitem__.
这意味着, d[k]和d.get(k)在查找键时可能会调用__missing__方法.

abc.Mapping子类, 以最简单的方式实现__getitem__方法
定义一个精简的abc.Mapping子类, 实现__missing__方法和必要的抽象方法, 
其中__getitem__方法不调用__missing__.
这个类永不触发__missing__方法.

abc.Mapping子类, 让__getitem__调用__missing__
定义一个精简的abc.Mapping子类, 实现__missing__方法和必要的抽象方法,
其中__getitem__方法调用__missing__.
d.get(k)和k in d遇到缺失的键将触发__missing__方法.

这种情况的代码件示例代码中的missing.py文件.
# 将代码拆分解释.

def _upper(x):
    try:
        # 返回全大写的键, 非字符串调用会抛出: 属性错误.
        return x.upper()
    # 捕获异常, 非字符串类型, 直接返回键.
    except AttributeError:
        return x
    
# 继承dict
class DictSub(dict):
    # 
    def __missing__(self, key):
        return self[_upper(key)]


# 实例对象, (A 映射 'letter A')
d = DictSub(A = 'letter A')
# 1. d['a'], 先调用父类dict的__getitem__方法, 查找属性a的值, 显然是找不到的.
# 2. 父类dict的__getitem__方法回落到实例的__missing__方法, 其中调用_upper函数将a转为A.
# 3. d['A']再调用父类dict的__getitem__方法, 查找属性A的值.
d['a']  # ✅
# Output: 'letter A'

# dict的默认行为.
d.get('a', '')
# Output: ''

# dict的默认行为.
'a' in d
# Output: False

from collections import UserDict

class UserDictSub(UserDict):
    def __missing__(self, key):
        return self[_upper(key)]

    
ud = UserDictSub(A = 'letter A')

# 继承UserDict的映射类, []与get都调用__getitem__方法.
ud['a']  # ✅
# Output: 'letter A'

ud.get('a', '')  # ✅
# Output: 'letter A'

'a' in ud
# Output: False

from collections import abc

class SimpleMappingSub(abc.Mapping):
    def __init__(self, *args, **kwargs):
        self._data = dict(*args, **kwargs)

    def __getitem__(self, key):
        return self._data[key]

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

sms = SimpleMappingSub(A = 'letter A')

# 自定义类中没有定义__missing__方法, 对象的属性是dict, 以下操作都是dict的默认行为.
sms['a']
# Output: KeyError: 'a'

sms.get('a', '')
# Output: ''

'a' in sms
# Output: False

# 上面中的SimpleMappingSub类
class MappingMissingSub(SimpleMappingSub):
    def __getitem__(self, key):
        try:
            return self._data[key]
        except KeyError:
            return self[_upper(key)]

mms = MappingMissingSub(A = 'letter A')
# 重写__getitem__方法, 小写的a不存在, 则装换为大写的A取值...
mms['a']  # ✅
# Output: 'letter A'

mms.get('a', '')  # ✅
# Output: 'letter A'

'a' in mms  # ✅
# Output: True

# 上面中的SimpleMappingSub类
class DictLikeMappingSub(SimpleMappingSub):
    def __getitem__(self, key):
        try:
            return self._data[key]
        except KeyError:
            return self[_upper(key)]

    def get(self, key, default=None):
        return self._data.get(key, default)

    def __contains__(self, key):
        return key in self._data

dms = DictLikeMappingSub(A = 'letter A')
dms['a']  # ✅
# Output: 'letter A'

dms.get('a', '')
# Output: ''

'a' in dms
# Output: False

以上4种情况的具体实现做了简化处理.
你在自定义的子类中实现__getitem__, get和__contains__方法时, 
可以根据实际需求决定是否使用__missing__方法.
本节的目的是指出, 继承标准库中的映射时要小心谨慎, 不同的基类对__missing__的使用方式不一样.

不要忘了, setdeault和update的行为也受键查找的影响.
最后, __missing__的逻辑容易出错, 有时候还要以特殊的逻辑实现__setitem__, 以免行为不一致或出乎意料.
3.6.5节有一个示例.

目前我们讨论了dict和defaultdict两种映射类型, 除此之外, 标准库中海油其他映射类型, 下面将逐一讨论.
3.6 dict 的变体
本节概述标准库中除defaultdict(3.5.1节已经介绍)之外的映射类型.
3.6.1 collections.OrdereDoct
自Python3.6, 内置的dict也保留键的顺序.
使用OrderedDict最主要的原因是编写与早期Python版本兼容的代码.
不过, dict和OrderedDict之间还有一些差异, Python文档中有说明, 
摘录如下(根据日常使用频率, 顺序有调整).

 OrderedDict 的等值检查考虑顺序.
 OrderedDict 的popitem()方法签名不同, 可通过一个可选参数指定移除哪一项.
 OrderedDict 多了一个move_to_end()方法, 便于把元素的位置移到某一端.
 常规的dict主要用于执行映射操作, 插入顺序是次要的.
 OrderedDict 的目的是方便执行重新排序操作, 空间利用率, 迭代速度和更新操作的性能是次要的.
 从算法上看, OrderedDict处理频繁重新排序操作的效果比dict好,
  因此适合用于跟踪近期存取情况(例如在LRU缓存中).
3.6.2 coolections.ChainMap
ChainMap实例存放一组映射, 可作为一个整体来搜索.
查找操作按照输入映射在构造函数调用中出现的顺序执行, 一旦在某个映射中找到指定的键, 旋即结束.
例如:
>>> d1 = dict(a=1, b=3)
>>> d2 = dict(a=2, b=4, c=6)
>>> from collections import ChainMap
>>> chain = ChainMap(d1, d2)
>>> chain['a']
1
>>> chain['c']
6

ChainMap实例不复制输入的映射, 而是存放映射的引用.
ChainMap的更新或插入操作只影响第一个输入映射.
续前例:
>>> chain['c'] = -1
>>> d1
{'a': 1, 'b': 3, 'c': -1}
>>> d2
{'a': 2, 'b': 4, 'c': 6}

ChainMap可用于实现支持嵌套作用域的语言解释器, 按嵌套层级从内到外, 一个映射表示一个作用域上下文.
collections文档中的'ChainMap objects' 一节举了几个ChainMap用法示例.
其中一个就是模仿Python查找变量的基本规则, 如下所示.
import builltins
pylookup = ChinaMap(locals(), global(), vars(builtins))
18章中的示例18-14使用一个ChainMap子类为Scheme编程语言的子集实现解释器.
3.6.3 collections.Counter
这是一种对键计数的映. 更新现有的键, 计数随之增加.
可用于统计可哈希对象的实例数量, 或者作为多重集(multiset, 本节后文讨论)使用.
Counter 实现了组合计数的+和-运算符, 以及其他一些有用的方法, 例如, most_common([n]). 
该方法返回一个有序元组列表, 对应前n个计算值最大的项及其数量.
下面使用Counter统计词中的字母数量.
>>> import collections
# 生成直方图
>>> ct = collections.Counter('abracadabra')
>>> ct
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

>>> ct.update('aaaaazzz')
>>> ct
Counter({'a': 10, 'z': 3, 'b': 2, 'r': 2, 'c': 1, 'd': 1})

>>> ct.most_common(3)
[('a', 10), ('z', 3), ('b', 2)]

注意, 'b''r'两个键并列第三, 但是ct.most_common(3)只显示3.

若想把collections.Counter当作多重集使用, 假设各个键是集合中的元素, 计算值则是元素在集合中出现的次数.
3.6.4 shelve.shelf
标准库中的shrlve模式持久存储字符串键与(以plckle二进制格式序列化的)Python对象之间的映射.
你可能觉得shrlve这个名字有点奇怪, 不过想到泡菜坛(pickle jar)是放在货架(shelve)上的, 还是有一定道理的.

模块级函数shelv.open返回一个shelve.Shelf实例, 这个一个简单的键值DBM数据库, 背后dbm模块.
shelve.Shelf具有以下特征.
 shelve.Shelf是abc.MutableMapping的子类, 提供了我们预期的映射类型基本方法.
 此外, shelve.Shelf还提供了一些其他I/O管理方法, 例如sync和close.
 Shelf实例是上下文件管理器, 因此可以使用with块确保在使用后关闭.
 为键分配新值后即保存键和值.
 键必须是字符串.
 值必须是pickle模块可以序列化的对象.

详细说明和一些注意事项见shelve模块, dbm模块和pickle模块的文档.
***----------------------------------------------------------------------------------------***
在极为简单的情况下, Python的pickle模块用着比较顺手, 但也是隐藏的陷阱也不少.
使用pickle解决问题之前, 请务必阅读Ned Batchelder写的"Pickle's nine flaws"一文.
Ned在这篇文章中给出了可供选择的其他序列化格式.
***----------------------------------------------------------------------------------------***
OrderedDict, ChainMap, Counter和shelf本身就是可用的, 此外也可以通过子类定制.
相反, UserDict只应作为基类, 在此基础上拓展.
3.6.5 子类应继承UserDict而不是dict
创建新的映射类型, 最好拓展collections.UserDict, 而不是dict.
为了确保以str类型存储添加到映射中的键, 我们在示例3-8中定义了StrKeyDict0类.
那时我们就意识到了这一点.

子类最好继承UserDict的主要原因是, 内置的dict在实现上走了一些捷径,
如果继承dict, 那就不得不覆盖一些方法, 而继承UserDict则没有这些问题. 
(7: 子类继承dict和其他内置类型的具体问题将在14.3节讨论.)

注意, UserDict没有继承dict, 使用的是组合模式: 内部有一个dict实例, 名为data, 存放具体的项.
与示例3-8相比, 这样做可以避免__setitem__等特殊方法意外递归, 还能简化__contains__的实现.

示例3-9的StrKeyDict继承UserDict, 实现过程比StrKeyDict0(示例3-8)更简洁,
而且功能更丰富: 所有见都以str类型存储, 使用包含非字符串键的数据结构或更新说不会发生意外情况.
# 示例3-9 StrKeyDict在插入, 更新和查找时, 时钟把非字符串键转为str类型
import collect


# StrKeyDict拓展UserDict.
class StrKeyDict(collections.UserDict):
    # __missing__与示例3-8完全相同.
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]
    
    def __contains__(self, key):
        # __contains__更简单, 因为存储的所有键都可以假定为str类型, 
        # 不用像StrKeyDict0那样调用self.keys(), 检查self.data即可.
        return str(key) in self.date
   	
    def __setitem__(self, key, item):
        # __setitem__把key转换成str类型. 
        # 委托给self.data属性之后, 这个方法更易于重写.
        self.data[str(key)] = item
        
由于UserDict拓展abc.MutableMapping, 因此使StrKetDict成为一种功能完整的映射的方法,
是从UserDict, MutableMapping或Mapping继承的方法.
后两个类虽然是抽象基类, 但是也有一些有用的具体方法.
这句话主要在讲述StrKeyDict类如何成为一个功能完整的映射(即支持映射的基本操作,
如__getitem__, __setitem__, __delitem__, __len__, __iter__`).

首先, StrKeyDict继承了UserDict类, 
这个类是一个类字典的实现, 它提供了一些方便的方法来操作字典, 例如get和pop等方法.

其次, UserDict类继承了MutableMapping抽象基类, 
这个抽象基类定义了一些必须实现的方法, 以便能够支持字典的基本操作.
具体来说, 它要求实现__getitem__, __setitem__, __delitem__, __iter__, 和__len__这五个方法.

最后, MutableMapping继承了Mapping抽象基类,
Mapping抽象基类是一个只读映射的基类, 它定义了只读映射必须实现的方法,
包括__getitem__, __iter__, __len__, 和keys, values, items等方法.

因此, 通过继承UserDict类, 它实现了MutableMapping抽象基类定义的方法,
同时通过MutableMapping的继承关系, 它也实现了Mapping抽象基类定义的方法,
这使得StrKeyDict类成为了一个功能完整的映射.
下面两个方法值得关注.

MutableMapping.update
这个强大的方法可以直接调用, __init__也使用它从其他映射, 键值对可迭代对象和关键字参数中加载实例.
该方法使用self[key] = value句法添加项, 最终会调用子类实现的__setitem__.

Mapping.get
在StrKeyDict0中(参见示例3-8), 我们必须自己实现get方法, 返回与__getitem__一样的结果.
而在示例3-9, 我们继承了Mapping.get, 它的实现与StrKeyDict0.get完全相同.
(Mapping.get的实现就是从collections.abc.Mapping类中继承下来的默认实现).
*---------------------------------------------------------------------------------------------*
Antoine Pitron编写'PEP 455--Adding a key-transforming dictionary to collections'
提议为collectionss模块增加TransformDict, 这比StrKeyDict更通用, 在转换之前保留键的原本类型.
PEP 455 20155月被否决, 拒绝原因见Raymond Hettinger的回应.
为了试验TransformDict, 我把Pitrou的补丁从18986号工单中提取出来了, 
制成了独立的模块(03-dict/set/transformdict.py).
*---------------------------------------------------------------------------------------------*
我们知道Python有不可变序列类型, 那有没有不可变映射呢?
其实, 标准库中没有, 不过有变通方案. 详见3.7.
3.7 不可变映射
标准库提供的映射类型都是可变的, 不过有时也需要防止用户意外更改映射.
3.5.2节提到的硬件编程库Pingo就有一例: board.pings映射表示设备上的GPIO物理引脚,
因此需要防止被人无意中更新, 毕竟软件不能更改硬件, 不然就与设备的实际物理结构不一致了.

types模块提供的MappingProxyType是一个包装类, 把传入的映射包装成一个mappingproxy实例,
这是原映射的动态代理, 只可读取.
这意味着, 是原映射的更新将体现在mappingproxy实例身上, 但是不能通过mappingproxy实例更改映射.
示例3-10简单演示MappingProxyType的用途.
# 示例3-10 MappingProxyTyoe根据dict对象构建只读的mappingproxy实例
>>> from types import MappingProxyType
>>> d = {1: 'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})

# d_proxy可以访问d中的项,
>>> d_proxy[1]
'A'

# 不能通过d_proxy更改映射.
>>> d_proxy[2] = 'x'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment

>>> d[2] = 'B'
# d_proxy是动态的, 能够反应d的变化.
>>> d_proxy
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'

在硬件编程场景中可以这样使用: 定义Board的具体子类, 在构造方法中声明一个私有映射, 存放引脚对象,
再使用mappingproxy实现一个公开的.pin属性, 通过API开放给客户端使用.
这样, 客户端就不会意外地添加, 删除或更改引脚.

接下来探讨视图.
通过视图可以对字典执行一些高性能操作, 免去了复制数据的麻烦.
3.8 字典视图
dict的实例方法 .keys(), .values(), .items()分别放返回dict_keys, dict_values和dict_items类的实例.
这些字典视图是dict内部实现使用的数据结构的只读投影.
Python2中对应的方法返回列表, 重复dict中已有的数据, 有一定的内存开销.
另外, 视图还取代了返回迭代器的旧方法.

示例3-11展示所有字典视图均支持的一些基本操作.
# 示例3-11 .value()方法返回dict对象的值视图
>>> d = dict(a=10, b=20, c=30)
>>> values = d.values()

# 通过视图对象的字符串表示形式查看视图的内容. 
>>> values
dict_values([10, 20, 30])

# 可以查询视图的长度.
>>> len(values)
3

# 视图是可迭代对象, 方便构建列表.
>>> list(values)
[10, 20, 30]

# 视图实现了__reversed__方法, 返回一个自定义迭代器.
>>> reversed(values)
<dict_reversevalueiterator object at 0x10e9e7310>

# 不嫩使用[]获取视图中的项.
>>> values[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict_values' object is not subscriptable

视图对象是动态代理.
更新原dict对象后, 现有的视图立刻就能看到变化. 续示例3-11:
>>> d['z'] = 99
>>> d
{'a': 10, 'b': 20, 'c': 30, 'z': 99}

>>> values
dict_values([10, 20, 30, 99])

dict_keys, dict_values和dict_items是内部类, 
不能通过__builtins__或标准库中的任何模块获取, 尽管可以得到实例, 但是在Python代码中不能自己动手创建.
# 获取类名
>>> values_class = type({}.values())
# 类名加括号实例化
>>> v = values_class()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot create 'dict_values' instances

dict_values类是最简单的字典视图, 只实现了__len__, __iter__和__reversed__这三个特殊方法.
除此以外, dict_keys和dict_items还实现了多个集合的方法, 基本与frozenset类相当.
讲解集合之后再深入探讨dict_key和dict_items, 详见3.13.
接下来讲一讲dict的内部实现产生的一些制约, 并给出一些实践小窍门.
3.9 dict的实现方式对实践的影响
Python使用哈希表实现dict, 因此字典的效率非常高, 不过这种设计对实践也有一些影响, 不容忽视.
 键必须是可哈希的对象. 
  就像3.4.1节所说的那样, 必须正确实现__hash__和__eq__方法.
  
 通过见访问项速度非常快.
  对于一个包含数百万个键的dict对象, Python通过计算键的哈希码就能直接定位键,
  然后找出索引在哈希表中的偏移量, 稍微尝试几次就能找打匹配的条目, 因此开销不大.
  
 在CPython3.6, dict的内存布局更为紧凑, 顺带一个副作用是键的顺序得以保留.
  Python3.7正式支持保留顺序.
  
 尽管采用了新的紧凑布局, 但是字典任然占用大量内存, 这是不可避免的.
  对容器来说, 最紧凑的内部数据结构是指向项的指针的数组. 
   与之相比, 哈希表中的条目存储的数据更多, 
  而且为了保证效率, Python至少需要把哈希表中三分之一的行留空.
  (8: 元组就是这样存储的.)
  
 为了节省内存, 不要在__init__方法之外创建实例属性.

最后一点背后的原因是, Python默认在特殊的__dict__属性中存储实例的属性, 
而这个属性的值是一个字典, 依附在各个实例上.
⑨自从Python3.3实现'PEP 412--Key-Sharing Dictionary'之后, 类的实例可以共用一个哈希表, 随类一起存储.
(9: 除非类有__slots__属性, 详见11.11.)
如果新实例与__init__返回的第一个实例拥有相同的属性名称, 
那么新实例的__dict__属性就共享这个哈希表, 仅以指针数组的形式存储新实例的属性值.
__init__方法执行完毕后再添加实例属性,
Python就不得不为这个实例的__dict__属性创建一个新哈希表(在Python3.3之前, 这是所有实例的默认行为).
根据PEP 412, 这种优化可将面向对象程序的内存使用量减少10%~20%.
上诉解释:
在Python中, 实例对象的属性通常是存储在一个名为__dict__的字典中.
每个实例对象都有一个独立的__dict__字典, 用于存储该实例的属性和对应的值.
当创建一个新的实例对象时, Python会为该实例分配一个独立的__dict__字典.

然而, 如果新创建的实例对象与__init__方法返回的第一个实例对象具有相同的属性名称,
Python会优化内存使用, 将新实例的__dict__属性与第一个实例共享同一个哈希表.
这意味着新实例的__dict__属性实际上是一个指针数组, 指向第一个实例的属性值.

然而, 一旦__init__方法执行完毕并为新实例添加了额外的属性,
Python就不得不为新实例的__dict__属性创建一个新的哈希表, 以容纳这些额外的属性.
这是因为新实例的属性与第一个实例的属性不再完全相同, 因此需要一个新的字典来存储新实例的属性和值.

这种优化可以帮助减少内存消耗, 特别是对于那些具有大量相同属性的实例对象.
通过共享哈希表, 可以节省内存空间.
然而, 需要注意的是, 一旦新实例添加了与第一个实例不同的属性,
就会引发哈希表的重新创建, 从而可能导致内存占用的增加.
下面开始研究集合.
3.10 集合论
集合对Python来说并不是新事物, 但目前尚未得到充分利用.
set类型及其不可变形式frozenset首先作为模块出现在Python2.3标准库中, 
随后在Python2.6中被提升为内置函数.
**-------------------------------------------------------------------------------------------**
本书使用'集合'一词指代set和frozenset.
如果讨论内容仅涉及set类, 则使用等宽字体, 写作set.
**-------------------------------------------------------------------------------------------**
集合是一组唯一的对象.
集合的基本作用是去除重复项.
>>> l = ['spam', 'spam', 'eggs', 'spam', 'bacon', 'eggs']
>>> set(l)
{'eggs', 'spam', 'bacon'}
>>> list(set(l)
['eggs', 'spam', 'bacon']
         
*-------------------------------------------------------------------------------------------*
如果想去除重复项, 同时保留每一项首次出现位置的顺序, 那么现在使用普通的dict即可, 如下所示.
*-------------------------------------------------------------------------------------------*
>>> dict.fromkeys(l).keys()
dict_keys(['spam', 'eggs', 'bacon'])

>>> list(dict.fromkeys(l).keys())
['spam', 'eggs', 'bacon']
集合元素必须是可哈希的对象.
set类型不可哈希对象, 因此不能构建嵌套set实例的set对象.
但是frozenset可以哈希, 所以set对象可以包含frozenste元素.

除了强制唯一性之外, 集合类型通过中缀运算法实现了许多集合运算.
给定两个集合a和b, a | b计算并集, a & b计算交集, a - b计算差集, a ^ b计算对称集.
巧妙使用集合即可以减少代码行, 也能缩减Python程序的运行时间, 同时还能少编写一些循环和添加逻辑,
从而让代码更易于阅读和理解.

举个例子, 假设有一个集合存储大量电子邮件地址(haystack), 还有一个集合存储少量电子邮件地址(needled),
你想统计needled中有多少邮件地址出现在haystack中.
捷足集合交集运算(&运算符), 用一行代码即可实现(见例3-12).
# 示例3-12 统计needles在haystack中出现的次数(二者均为集合类型)
found = len(needles & haystack)

如果不使用交集运算符, 则要像示例3-13那样编写代码, 才能实现与示例3-12一样的效果.
# 示例3-13 统计needles在haystack中出现的次数(效果与示例一样)
found = 0
for n in needles:
	if n in haystack:
		fount += 1
        
示例3-12的运行速度比示例3-13稍快.
不过, 示例3-13处理的needles和haystack可以是任意可迭代对象, 而示例3-12要求二者均为集合.
然而, 就算一开始不是集合, 也可以快速构建, 如示例3-14所示.
# 示例3-14 统计needles在haystack中出现的次数, 支持任何可迭代类型
found = len(set(needles) & set(haystack))

# 另一种方式
found = len(set(needles).intetsection(haystack))

当然, 像示例3-14那样构建集合有一个额外的开销.
但是, 如果needles和haystack中有一个本身就是集合, 那么示例3-14中的第二种方式比示例3-13开销更小.

假如needles中有1000个元素, haystack中有10 000 000, 那么前面几例的运行时间都在0.3毫秒左右,
平摊到每一个元素上约为0.3微秒.

成了成员测试速度极快(归功于底层哈希表)之外, 内置类型set和drozenset还提供了丰富的API,
有的用于创建新集合, 有的用于更改set对象.
在讨论这些操作之前, 先讲一讲句法.
3.10.1 set字面量
set字面量的句法与集合的数字表示法几乎一样, 例如{1}, {1, 2}.
唯有一点例外: 空set没有字面量表示法, 必须写作set().
***--------------------------------------句法陷阱--------------------------------------------***
创建空set, 务必使用不带参数的构造函数, 即set().
倘如写成{}, 则创建的是空dict--这一点在Python中没有变化.
***-----------------------------------------------------------------------------------------***
在Python3中, 几何的标准字符串表形式始终使用{...}表示, 唯有空集例外.
>>> s = {1}
>>> type(s)
<class 'set'>

>>> s
{1}

>>> s.pop()
1

>>> s
set()

与调用构造函数(例如 set([1, 2, 3]))相比,
使用set的字面量句法(例如{1, 2, 3})不仅速度快, 而且更具可读性.
调用构造函数速度慢的原因是, Python要查找set名称, 找出构造函数,
然后构建一个列表, 最后再把列表传给构造函数.

相比之下, Python处理字面量只需要运行一个专门的BUILD_SET字节码. 
(10: 这可能很有趣, 但不是特别重要. 
只有求解一个集合字面是才有加速效果, 而且每个Python进程最多发生一次, 即首次编译模块时.
如果你好奇, 可以从dis模块导入dis函数, 
反汇编set字面量(例如dis('{1}'))和set调用(例如dis('set([1])'))的字节码.)

frozenset没有字面量句法, 必须调用构造函数创建.
在Python3中, frozenset的字符串表示形式类似于构造函数调用(字符串表示形式和调用方式长的一样).
下面是控制台会话的输出.
>>> frozenset(range(10))
frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

说到句法, 列表推导式的思想也可以用于构建集合.
3.10.2 集合推导式
结合推导式早在Python2.7就已出现, 3.2.1节讲到的字典推导式同时引入.
示例3-15演示集合推导式的用法.
# 示例3-15 构件一个集合, 元素为Unicode名称中带有'SIGN'一词的Latin-1字符
# 从unicodedata中导入name函数, 获取字符名称.
>>> from unicodedata import name
# 将代码在32~255的范围内, 而且名称中带有'SIGN'一词的字符串放入集合.
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}
{'#', '<', '§', '£', '¤', '÷', '¶', 'µ', '©', '°', '=', 
 '¥', '%', '$', '¬', '±', '>', '¢', '+', '®', '×'}

不同的Python进程得到的输出顺序不一样, 原因与3.4.1节提到的加盐哈希有关,
句法讲完了, 接下来讨论集合的行为.
3.11 集合的实现方式对实践的影响
set和frozenset类型都使用哈希表实现. 这种设计带来了以下影响.
 集合元素必须是哈希对象, 必须像3.4.1节所说那样正确实现__hash__和__eq__方法.

 成员测试效率非常高. 
  对于一个包含数百万个元素的集合, 计算元素的哈希码就可以直接定位元素, 找出元素的索引偏移量.
  稍微搜索几次就能找打匹配的元素, 即使穷经搜索开销也不大.
  
 与存放原始指针的低层数组相比, 集合占用大量内存.
  尽管集合的结构更紧凑, 但是一旦要搜索元素数量变多, 搜索速度将显著下降.
  
 元素的顺序取决于插入顺序, 但是顺序对集合没有什么意义, 也得不到保障.
  如果两个元素具有相同的哈希码, 则顺序取决于哪个元素先被添加到集合里.
  
 向集合中添加元素后, 现有元素的顺序可能发生变化.
  这是应为哈希表使用率超过三分之二后, 算法效率会有所下降, Python可能需要移动和调整哈希表大小,
  然后重新插入元素, 导致元素的相关顺序发生变化.
接下来讲解丰富的集合运算.
3.11.1 集合运算
3-2概括了可变和不可变集合使用的方法.
其中很多方法是重载运算符(例如&>=)的特殊方法.
3-2列出数学上的集合运算符和对应的Python运算符或方法.
注意, 一些运算符和方法就地更改目标集合, 例如&=, difference_update等.
这样的运算对理想世界中的数学集合没有意义, 而且frozenset没有实现.
*-------------------------------------------------------------------------------------------*
3-2列出的中缀运算符要求两个操作数均为集合, 其他方法则接受一个或多个可迭代对象参数.
比如说, 为了计算4个容器a, b, c和d的并集, 可以调用a.union(b, c, d), 其中a必须是集合,
而b, c和d可以使任何类型的可迭代对象, 只要项是可哈希的对象即可.
Python3.5实现'PEP 448--Additional Unpacking Generalizations'之后,
如果你想要使用4个可迭代对象合并后的结果创建一个集合, 
那么无须在现有的集合上更新, 加可以使{*a, *b, *c, *d}句法.
*-------------------------------------------------------------------------------------------*

图3-2

3-2: MutableSet及其子类collentions.abc中的超类的简化UML类图
(以斜体显示的名称是抽象类和抽象方法: 简单起见, 省略了反向运算符方法)

  交集
  A⊂B,  A属于B
  A⊃B,  A包括B
 a∈A,  a是A的元素
  A⊆B,  A不大于B
  A⊇B,  A不小于B
Ø  空集
3-2: 集合数学运算符*要么生成一个新集合, 要么就地更新可变集合)

数学符号  Python运算符   方法                               说明
S  Z     s & z         s.__add__(z)                       s和z的交集
          z & s         s.__radd__(z)                      反向&运算符    
                        s.intersection(it, ...)            s和根据可迭代对象it等构建的集合的交集
          s &= z        s.__iadd__(z)                      使用s和z的交集更新s
                        s.intersection_update(it, ...)     使用s和根据可迭代对象it等
                                                           构建的集合的交集更新s
                      
S  Z    s | z          s.__or__(z)                       s和z的并集
	     z | s          s.__ror__(z)                      反向|运算符
	                    s.union(it, ...)                  s和根据可迭代对象it等构造的集合的并集
	     s |= z         s.__ior__(z)                      使用s和z的并集更新s
	                    s.update(it, ...)                 使用s和根据可迭代对象it等
	                                                      构建的集合的并集更新s
	                  
S \ Z    s - z          s.__sub_(z)                       s和z的相对补集(或差集) 
         z - s          s.__rsub__(z)                     反向-运算符
                        s.difference(it, ...)             s和根据可迭代对象it等构建的集合的差集
         s -= z         s.__isub__(z)                     使用s和z的差集更新s
                        s.differrnce_update(it, ...)      使用s和根据可迭代对象id等
                                                          构建的集合差集更新s
                                                          
S△Z     s ^ z          s.__xor__(z)                      对称差集(s & z的补集)
         z ^ s          s.__rxor__(z)                     反向^运算符
                        s.symmetric_difference(it)        s & set(it)的补集
         s ^= z         s.__ixor__(z)                     使用s和z的对称差集跟新s
         	           s.symmetric_difference_update(it, ...) 使用s和根据可迭代对象it等
         	                                              构建的集合的对称差集更新s
3-3 列出了集合谓词, 即返回True或False的运算符和方法.
3-3: 返回布尔值的集合比较运算符和方法

数学符号   Python运算符  方法                说明
S  Z = Ø               s.isdisjoint(z)     s和z不相交(没有共同元素)
e  S     e in s       s.__contains__(e)   元素e是s的成员
S  Z     s <= z        s.__le__(z)         s是z的子集
                        s.issubset(it)      s是由可可迭代对象it构建的集合的子集
                        
A  Z     s < z         s.__lt__(z)         s是z的真子集
S        s >= z        s.__ge__(z)         s是z的超集
                        s.issuperset(it)    s是由可迭代对象it构建的集合的超级
                        
S  Z     s > z         s.__gt__(z)        s是z的真超集
除了源自数学集合论的运算符和方法之外, 集合类型还实现了一些其他实用方法, 如表3-4所示.
3-: 集合的其他方法

方法             set      frozenset       说明
s.add(e)                                 把元素e添加到s中
s.clear()                                删除s中的全部元素
s.copy()                                浅拷贝s
s.discard(e)                             从s中删除元素e(如果存在e)              
s.__iter__()                            获取遍历s的迭代器
s.__len__()                             len(s)
s.pop()                                  从s中删除并返回一个元素, 如果s为空, 则抛出KeyError
s.remove()                               从s中删除元素e, 如果e不在s中, 则抛出KeyError
对集合功能的概述到此结束.
3.12节将兑现3.8节的承诺, 探讨与frozenset行为非常相似的两种字典视图.
3.12 字典视图的集合运算
.keys().items()这两个dict方法返回的视图对象与frozenset极为相似, 如表3-5所示.
3-5: frozenset, dict_keys和dict_items实现的方法

方法                       frozenset  dict_keys  dict_values   说明
s.__add__(z)                                                s $ z (s和z的交集)
s.__rand__(z)                                               反向 & 运算符
s.__contains__()                                            e in s
s.copy()                                                      浅拷贝s
s.difference(it, ...)                                         s和可迭代对象it的差集
d.intersection(it, ..)                                        s和可迭代对象it的交集
s.isdisjoint(z)                                             s和z不相交(没有共同的元素)   
s.issubset(it)                                                s是可迭代对象it的子集
s.issuperset(it)                                              s是可迭代对象的超集
s.__iter__()                                                获取遍历s的迭代器
s.__len__()                                                 len(s)
s.__or__(z)                                                 s | z (s和z的并集)
s.__ror__()                                                 反向|运算符
s.__reversed__()                                             获取逆序遍历s的迭代器
s.__sub__(z)                                                s - z (s和z的差集)
s.__rsub__(z)                                               反向-运算符
s.symmetric_difference(it)                                    s & set(it)的补集
s.union(it, ...)                                              s和可迭代对象it等的并集
s.__xor__()                                                 s ^ z (s和z的对称差集)
s.__rxor__()                                                反向^运算符
需要特别注意的是, dict_keys和dict_items实现了一些特殊方法, 支持强大的集合运算符,
包括&(交集), |(并集), -(差集)^(对称集).

例如, 使用&运算法可以轻易获取两个字典都有的键.
>>> d1 = dict(a=1, b=2, c=3, d=4)
>>> d2 = dict(b=20, d=40, e=50)
>>> d1.keys() & d2.keys()
{'b', 'd'}

注意, &运算符返回一个set.
更方便的是, 字典视图的集合运算符均兼容set示例, 如下所示.
>>> s = {'a', 'e', 'i'}
>>> d1.keys() & s
{'a'}
>>> d1.keys() | s
{'a', 'c', 'b', 'd', 'i', 'e'}

***-----------------------------------------------------------------------------------------***
仅当dict中所有值均可哈希时, dict_items视图才可当作集合使用.
倘若dict中有不可哈希的值, 对dict_items视图做集合运算将抛出TypeError:
unhashable type 'T', 其中T是不可哈希的类型.
相反, dict_keys视图实始终可当作集合使用, 因此按照其设计, 因为按照其设计, 所有键均可哈希.
***-----------------------------------------------------------------------------------------***
使用集合运算符处理视图可以省去大量循环和条件判断.
繁重工作都可以交给C语言实现的Python高效完成.
3.13 本章小结
字典是Python的基石.
近些年, 我们熟悉的字面量句法{k1: v1, k1: v2}有所增强, 现在支持使用**拆包, 模式匹配和字典推导式.
除了基本的dict之外, 标准库中的collections模块还提供了随取随存的专门映射,
例如, defaultdict, ChainMap, Counter.
重新实现dict之后, OrderedDict不像以前那样有用了, 但仍然应该留在标准库中, 一方面是为了向后兼容,
另一方面OrseredDict还有一些特殊是dict不具备的, 例如比较运算符==把键的顺序纳入考虑范围.
collections模块中的UserDict是一个基类, 供用户自定义映射.

多数映射具有两个强大的方法, setdefault和update.
setdefault方法可以更新可变值的项(例如list值), 从而避免再次搜索相同的键.
update方法可以批量插入或覆盖项, 项可以来自提供键值对的可迭代对象, 也可以来自关键字参数.
映射构造函数在内部也使用update, 以便你根据映射, 可迭代对象或关键字参数初始化实例.
从Python3.9开始, hiahia可以使用|=运算符更新映射, 以及使用|运算符根据两个映射的合集创建新映射.

映射API中的__missing__方法是一个巧妙的钩子, 
利用它可以自定义d[k]句法(调用__getitem__)找不到键时的行为.
(抽象基类是Python中用于定义接口的类, 它们不能直接实例化, 而是用作其他类的基类.
抽象基类定义了一组方法, 子类需要实现这些方法以符合接口规范.)
collections.abc模块中的抽象基类Mapping和MutableMapping定义标准接口, 可以运行时检查类型.
types模块中的MappingProxyType为映射包装了一层不可变外壳, 防止意外更改映射.
Set和MutableSet也有抽象基类.

Python3引入的字典视图实一个很好的特性, 
消除了Python2中.keys(), .values().items()方法开销的内存, 不用再构建列表, 重复目标dict实例中的数据.
此外dict_keys和dict_items类还支持frozenset的一些最有用的运算符,
3.14 延伸阅读
Python标准库文档中的'collections--Container datatypes'有一些示例个实践技巧, 涵盖多种映射类型.
如果你想创建新映射类型, 或者了解现有映射的逻辑, 
collections模块的Python源码(Lib/collections/__init__.py)是不可错过的参考.
<<Python Cookbook 中文版(3)>>1章有20个实用的经典示例,
深入挖掘数据结果的用法, 尤其是dict的使用技巧.

Greg Gandenberger在'Python Dictionaries Are Now Ordered. Keep Keep Using OrderedDict'
一文中提倡继续使用collections.OrderedDict, 理由有三:
一是'明确胜于模糊', 二是向后模式, 三是一些工具和库假定dict键的顺序无关紧要.
(第三点的意思: 这些工具和库可能会将字典视为无序的数据结构, 并假设字典的键的顺序对其功能没有影响.
因此, 如果你在使用这些工具或库时使用了普通的字典(dict)它们可能会忽略或更改键的顺序.)

Guido van Rossum在'PEP 3106--Revamping dict.keys(), .values() and .items()'
中提出为Python3增加字典视图功能.
摘要指出, 这个想法源自Java Collections Framework.

在几个Python解释器中, PyPy第一个实现了Raymond Hettinger提议的紧凑字典,
详见博客文章'Faster, more memory efficient and more ordered dictionaries on PyPy'.
文中承认, PHP7也采用了类似地布局, 详见"PHP's new hashtable implementation"一文.
指明出处值得肯定.

在PyCon2017, Brandon Rhodes做了题为'The Dictionary Even Mightier'的演讲,
这是经典动画演讲'The Mighty Dictonary'的续集, 用动画演示了哈希碰撞.
Paymond Hettinger的演讲'Modern Dictionary'对dict的内部机制剖析更为深入.
在演讲中他提到, 最初他先向CPython核心开发团队建议实现更紧凑字典, 但无果而终, 后来成功游说PyPy团队,
引起CPython团队的关注, 而后由INADA Naoki在CPython3.6中实现.
详细信息参阅CPython源码Objects/dictobject.c中的大量注释和设计文档Objects/dictnotes.txt.

为Python添加集合的根本原因见'PEP 218--Addding a Built-In Set Object Type'.
PEP218被批准时, 尚未为集合提供专门的子面量句法.
set字面量, 连同字典和拖导式, 在Python3中才实现, 并向后移植到Python2.7.
在PyCon2019, 我做了题为"Set Practice: learning from Python's set types"的演讲,
通过具体的程序说明了集合的用途, 介绍了集合API的设计思路和uintset的实现方式.
uintset是一个存放整数元素的集合类, 使用的是位向量, 而不是哈希表, 
灵感来自<<Go 程序设计语言>>6章的一个示例.

IEEE出版的杂志Spectrum有一篇关于Hans Peter Luhn的报道,
他是一位多产的发明家, 增获得一种穿孔片组的专利, 根据可用的成分选择鸡尾酒配方.
在他众多的发明中就包括哈希表. 详见:'Hans Peter Luhn and the Birth of the Hashong Algorithm'一文.
*--------------------------------------------杂谈---------------------------------------------*
语法糖
我的朋友Geraldo Cohen 说过, Python'简单又正确'.
编程语言纯粹主义者认为句法并不重要.
	语法糖诱发分号癌. 
														---- Alan Perlis
														
句法是编程语言的用户界面, 在实践中很重要.
发现Python之前, 我使用Perl和PHP做过一些web编程.
这些语言的映射句法非常好用, 在不得不使用Java或C语言的日子里, 总是令我怀念.
好的映射字面量句法用着十分便利, 方便配置, 方便实现表驱动设计, 还方便存放原型设计和测试数据.
Go语言的设计人员就从动态语言中学到了这一点.
由于缺乏在代码中表达结构化数据的好方法, Java社区不得不采用烦琐的XML作为数据格式.

JSON是XML的一种替代方案, 取繁从件, 取得了巨大成功, 在很多情况下可以取代XML.
列表格字典的句法简单明了, 使JSON成为一种出色的数据交换格式.

PHP和Ruby模仿Perl的哈希句法, 使用=>链接键和值.
JavaScript与Python一眼, 使用: 一个字符串就能表明意图, 为什么要用两个呢?  
(11: 从Ruby1.9开始, 如果哈希的键时Symbol类型, 也可以使用: 字符. --译者注)

JSON源自于JavaScript, 无巧不成书, 它几乎就是Python句法的子集.
除了true, false和null等值的拼写不一样之外, JSON与Python几乎完全兼容.

Armin Ronacher在一篇推文中说, 
他喜欢在Python的全局命名空间中为Python的True, False和None添加兼容JSON的别名,
这样就可以直接把JSON粘贴到Python控制台中了, 就像下面这样.


>>> true, false, null = True, False, None
>>> fruit = {
...		"type": "banana",
...		"avg_weight": 123.2,
... 	"edible_peel": false,
...		"species": ["acuminata", "balbisiana", "paradisiace"],
...		"issues": null,
... }
>>> fruit
{'type': 'banana', 'avg_weight': 123.2, 'edible_peel': False,
'species': ['acuminata', 'balbisiana', 'paradisiace'], 'issues': None}

现在, 人人都使用Python的dict和list句法交换数据.
如今, 句法越来越便利, 还能保留插入顺序.

真是简单又正确.
*---------------------------------------------------------------------------------------------*
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值