学习《流畅的Python》 第3章 字典和集合

泛映射类型


collections.abc模块中有Mapping和MutableMapping这两个抽象基类,它们的作用是为dict和其他类似的类型定义形式接口。

from collections import abc

my_dict = {}
a = isinstance(my_dict, abc.Mapping)
print(a) # True

isinstance(object, classinfo) 
如果参数object是classinfo的实例,或者object是classinfo类的子类的一个实例, 返回True。如果object不是一个给定类型的的对象, 则返回结果总是False。


标准库里的所有映射类型都是利用dict来实现的,因此它们都有一个共同的限制,即只有**可散列的数据类型**才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的数据类型)。
关于可散列的数据类型可以直接参考百度:[Python可散列对象](https://jingyan.baidu.com/article/0320e2c11373d01b87507b9a.html)


对于元组,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。

tt = (1, 2, (30, 40))
print(hash(tt)) # 8027212646858338501
tl = (1, 2, [30, 40])
print(hash(tl)) # TypeError       Traceback (most recent call last)
                # in ()
                #     2 print(hash(tt))
                #     3 tl = (1, 2, [30, 40])
                # ----> 4 print(hash(tl))
                #     5 tf = (1, 2, frozenset([30, 40]))
                #     6 print(hash(tf))
                # TypeError: unhashable type: 'list'
                # 列表是不可散列的
tf = (1, 2, frozenset([30, 40]))
print(hash(tf)) # 985328935373711578

创建字典的不同方式


a = dict(one=1, two=2, three=3)
b = {'one':1, 'two':2, 'three':3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('one', 1), ('two', 2), ('three', 3)])
e = dict({'three': 3, 'one':1, 'two':2})
print(a == b == c == d == e) # True

字典推导


字典推导(diccomp)可以从任何以键值对作为元素的可迭代对象中构建出字典。

'''字典推导的应用-利用字典推导将装满元组的列表变成两个不同的字典'''
DIAL_CODES = [ # 一个承载成对数据的列表,它可以直接用在字典的构造方法中
        (86, 'China'),
        (91, 'India'),
        (1, 'United States'),
        (62, 'Indonesia'),
        (55, 'Brazil'),
        (92, 'Pakostan'),
        (880, 'Bangladesh'),
        (234, 'Nigeria'),
        (7, 'Russia'),
        (81, 'Japan')
]
country_code = {country: code for code, country in DIAL_CODES} # 国家名是键,区域码是值
print(country_code) # {'China': 86, 'India': 91, 'United States': 1, 'Indonesia': 62, 
                    # 'Brazil': 55, 'Pakostan': 92, 'Bangladesh': 880, 
                    # 'Nigeria': 234, 'Russia': 7, 'Japan': 81}
print(
    {code: country.upper() for country, code in country_code.items() if code < 66} # 用区域码作为键,国家名变为大写
) # {1: 'UNITED STATES', 62: 'INDONESIA', 55: 'BRAZIL', 7: 'RUSSIA'}

集合论


集合的本质是许多唯一对象的聚集。因此,集合可以用于去重:

#%% 集合用于去重
l = ['spam', 'spam', 'eggs', 'spam']
print(set(l)) # {'eggs', 'spam'}
print(list(set(l))) # ['eggs', 'spam']

集合字面量

除空集之外,集合的字面量————{1}、{1, 2},等等看起来跟它的数学形式一模一样。

如果是空集,那么必须写成set()的形式。如果写成{}形式,那么这是一个空字典。

#%% 
s = {1}
print(type(s)) # <class 'set'>
print(s) # {1}
print(s.pop()) # 1
print(set) # <class 'set'>
  • 集合推导

#%% 集合推导
from unicodedata import name

print({chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), ' ')}) # {'+', '£', '#', '®', '¤', '¥', '÷', '×', '±', 'µ', '¬', '§', '©', '°', '¢', '%', '$', '=', '>', '¶', '<'}
  • 集合的操作

dict和set的背后 

结论:如果在你的程序里有任何的磁盘输入/输出,那么不管查询多少个元素的字典或集合,所耗费的时间都能忽略不计。但是对于列表来说,由于列表的背后没有散列表来支持in运算符,每次搜索都需要扫描一次完整的列表,导致所需的时间根据haystack的大小呈线性增长。

字典中的散列表

散列表其实是一个稀疏数组。散列表里的单元通常叫做表元。在dict的散列表中,每个键值对都占用一个表元,每个表元有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取每个表元。

dict的实现及其导致的结果

键必须是可散列的

(1) 支持 hash() 函数,并且通过 __hash__() 方法所得到的散列值是不变的。
(2) 支持通过 __eq__() 方法来检测相等性。
(3) 若 a == b 为真,则 hash(a) == hash(b) 也为真。所有由用户自定义的对象默认都是可散列的,因为它们的散列值由id() 来获取,而且它们都是不相等的。

字典在内存上的开销巨大

由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例而言,如果你需要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是比较好的选择;最好不要根据 JSON 的风格,用由字典组成的列表来存放这些记录。用元组取代字典就能节省空间的原因有两个:其一是避免了散列表所耗费的空间,其二是无需把记录中字段的名字在每个元素里都存一遍。在用户自定义的类型中,__slots__ 属性可以改变实例属性的存储方式,由 dict 变成 tuple。记住我们现在讨论的是空间优化。如果你手头有几百万个对象,而你的机器有几个 GB 的内存,那么空间的优化工作可以等到真正需要的时候再开始计划,因为优化往往是可维护性的对立面。

键查询很快

dict 的实现是典型的空间换时间:字典类型有着巨大的内存开销,但它们提供了无视数据量大小的快速访问——只要字典能被装在内存里。正如表 3-5 所示,如果把字典的大小从 1000 个元素增加到 10 000 000 个,查询时间也不过是原来的 2.8 倍,从 0.000163秒增加到了 0.00456 秒。这意味着在一个有 1000 万个元素的字典里,每秒能进行 200 万个键查询。

键的次序取决于添加顺序

当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安排存放到另一个位置。于是下面这种情况就会发生:dict([key1, value1), (key2, value2)] 和 dict([key2,value2], [key1, value1]) 得到的两个字典,在进行比较的时候,它们是相等的;但是如果在 key1 和 key2 被添加到字典里的过程中有冲突发生的话,这两个键出现在字典里的顺序是不一样的。

# 世界人口数量前10位国家的电话区号
DIAL_CODES = [
    (86, 'China'),
    (91, 'India'),
    (1, 'United States'),
    (62, 'Indonesia'),
    (55, 'Brazil'),
    (92, 'Pakistan'),
    (880, 'Bangladesh'),
    (234, 'Nigeria'),
    (7, 'Russia'),
    (81, 'Japan'),
]
d1 = dict(DIAL_CODES) ➊
print('d1:', d1.keys())
d2 = dict(sorted(DIAL_CODES)) ➋
print('d2:', d2.keys())
d3 = dict(sorted(DIAL_CODES, key=lambda x:x[1])) ➌
print('d3:', d3.keys())
assert d1 == d2 and d2 == d3 ➍

➊ 创建 d1 的时候,数据元组的顺序是按照国家的人口排名来决定的。
➋ 创建 d2 的时候,数据元组的顺序是按照国家的电话区号来决定的。
➌ 创建 d3 的时候,数据元组的顺序是按照国家名字的英文拼写来决定的。
➍ 这些字典是相等的,因为它们所包含的数据是一样的。

往字典里添加新键可能会改变已有键的顺序

无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。要注意的是,上面提到的这些变化是否会发生以及如何发生,都依赖于字典背后的具体实现,因此你不能很自信地说自己知道背后发生了什么。如果你在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环很有可能会跳过一些键——甚至是跳过那些字典中已经有的键。

由此可知,不要对字典同时进行迭代和修改。如果想扫描并修改一个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。

set的实现以及导致的结果

set 和 frozenset 的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。在 set 加入到 Python 之前,我们都是把字典加上无意义的值当作集合来用的。在 节中所提到的字典和散列表的几个特点,对集合来说几乎都是适用的。为了避免太多重复的内容,这些特点总结如下。

  • 集合里的元素必须是可散列的。
  • 集合很消耗内存。
  • 可以很高效地判断元素是否存在于某个集合。
  • 元素的次序取决于被添加到集合里的次序。
  • 往集合里添加元素,可能会改变集合里已有元素的次序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值