《Python编程从入门到实践》
字典
使用字典
- 字典是一系列的键-值对
- 与键相关联的值可以使数字、字符串、列表乃至字典
- 在Python中,字典用放在花括号{}中的一系列键-值表示
- alien_0 = {'color': 'green', 'points': 5}
- 键-值对是两个相关联的值。指定键时,Python将返回与之相关联的值。键和值之间用冒号分隔,而键-值对之间用逗号分隔
- 要获取字典中的值,可依次指定字典名和放在方括号内的键
遍历
- 遍历所有的键值对
- 遍历所有的键或值
嵌套
- 有时候,需要将一系列字典存储在列表中,或将列表作为值存储在字典中,这成为嵌套。
在字典中存储列表
字典中存储字典
《Python 基础教程》
OneNote笔记百度网盘(PDF):
链接:https://pan.baidu.com/s/1Zv9q61C_LigU6yEWYY_niQ
提取码:i4w0
《流畅的Python》
泛映射类型
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 之前,我们都是把字典加上无意义的值当作集合来用的。在 节中所提到的字典和散列表的几个特点,对集合来说几乎都是适用的。为了避免太多重复的内容,这些特点总结如下。
- 集合里的元素必须是可散列的。
- 集合很消耗内存。
- 可以很高效地判断元素是否存在于某个集合。
- 元素的次序取决于被添加到集合里的次序。
- 往集合里添加元素,可能会改变集合里已有元素的次序。