字典和集合的相关知识梳理

字典推导

列表推导和生成器表达式的概念就移植到了字典上,从而有了字典推导(后面还会看到集合推导)。
字典推导(dictcomp)可以从任何以键值对作为元素的可迭代对象中构建出字典。

>>> DIAL_CODES = [                  #➊ 
...         (86, 'China'), 
...         (91, 'India'), 
...         (1, 'United States'), 
...         (62, 'Indonesia'), 
...         (55, 'Brazil'), 
...         (92, 'Pakistan'), 
...         (880, 'Bangladesh'), 
...         (234, 'Nigeria'), 
...         (7, 'Russia'), 
...         (81, 'Japan'), 
...     ] 
>>> country_code = {country: code for code, country in DIAL_CODES}  #➋ 
>>> country_code 
{'China': 86, 'India': 91, 'Bangladesh': 880, 'United States': 1,  
'Pakistan': 92, 'Japan': 81, 'Russia': 7, 'Brazil': 55, 'Nigeria':  
234, 'Indonesia': 62} 
>>> {code: country.upper() for country, code in country_code.items()  #➌ 
...   if code < 66} 
{1: 'UNITED STATES', 55: 'BRAZIL', 62: 'INDONESIA', 7: 'RUSSIA'}

➊ 一个承载成对数据的列表,它可以直接用在字典的构造方法中。
➋ 这里把配好对的数据左右换了下,国家名是键,区域码是值。
➌ 跟上面相反,用区域码作为键,国家名称转换为大写,并且过滤掉区域码大于或等于
66的地区。

集合论

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

>>> l = ['spam', 'spam', 'eggs', 'spam'] 
>>> set(l) 
{'eggs', 'spam'} 
>>> list(set(l)) 
['eggs', 'spam']

集合中的元素必须是可散列的,set类型本身是不可散列的,但是frozenset可以。因此
可以创建一个包含不同frozenset的set。

集合还实现了很多基础的中缀运算符 。
给定两个集合a和b,
a | b返回的是它们的合集,
a & b得到的是交集,
而a - b得到的是差集。

合理地利用这些操作,不仅能够让代码的行数变少,还能减少Python程序的运行时间。

#needles的元素在haystack里出现的次数,两个变量都是set类型
found = len(needles & haystack)

如果不使用交集操作的话,代码可能就变成了下面:

found = 0 
for n in needles: 
    if n in haystack: 
        found += 1

如果对象不是集合的话

#needles的元素在haystack里出现的次数,这次的代码可以用在任何可迭代对象上,,对对象没有要求,可以随时建立集合。
found = len(set(needles) & set(haystack)) 

集合字面量

除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的数学形式一模一样。
如果是空集,那么必须写成set()的形式

如果要创建一个空集,你必须用不带任何参数的构造方法set()。如果只是写成{}的形式,跟以前一样,你创建的其实是个空字典。

除了空集,集合的字符串表示形式总是以{…}的形式出现。

>>> s = {1} 
>>> type(s) 
<class 'set'> 
>>> s 
{1} 
>>> s.pop() 
1 
>>> s 
set()

像{1, 2, 3}这种字面量句法相比于构造方法(set([1, 2, 3]))要更快且更易读

集合推导

#新建一个Latin-1字符集合,该集合里的每个字符的Unicode名字里都有“SIGN”这个单词
>>>from unicodedata import name  #➊
>>>{char(i) for i in range(32,256) if 'SIGN'in name(char(i),'')}  #➋ 
{'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©',  
'°', '+', '÷', '±', '>', '¬', '®', '%'}

➊ 从unicodedata模块里导入name函数,用以获取字符的名字。
➋ 把编码在32~255之间的字符的名字里有“SIGN”单词的挑出来,放到一个集合里。
跟句法相关的内容就讲到这里,下面看看用于集合类型的丰富操作。

集合的操作

集合的数学运算:这些方法或者会生成新集合,或者会在条件允许的情况下就地修改集合
在这里插入图片描述
集合的 较运算符,返回值是布尔类型
在这里插入图片描述
集合类型的其他方法
在这里插入图片描述

dict和set的背后

字典中的散列表

散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。在dict的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。

如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。Python中可以用hash()方法来计算散列值

内置的hash()方法可以用于所有的内置类型对象。如果是自定义对象调用hash()的话,
实际上运行的是自定义的__hash__。如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。

如果一个对象的哈希值在其生存期内从未改变(它需要一个hash方法),并且可以与其他对象进行比较(它需要一个hash方法),那么该对象是散列的。

比较相等的哈希对象必须具有相同的哈希值。

散列性使对象可用作字典键和集合成员,因为这些数据结构在内部使用散列值。

Python的大多数不可变内置对象都是散列的;不可变容器(如列表或字典)不是; 不可变容器(如元组和frozenset)只有在其元素是散列的情况下才是散列的。默认情况下,作为用户定义类实例的对象是可散列的。 它们都比较不相等(除了它们自己),它们的散列值是从它们的id()派生的。

>>>tt = (1,2,(30,40))
>>>hash(tt)
8027212646858338501
>>>t1 = (1,2,frozenset([30,40]))
>>>hash(t1)
985328935373711578

散列冲突

从字典中取值的算法流程图;给定一个键,这个算法要么返回一个值,要么抛出KeyError异常
在这里插入图片描述

dict的实现及其导致的结果

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

2. 字典在内存上的开销巨大
由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。

3. 键查询很快
dict的实现是典型的空间换时间:字典类型有着巨大的内存开销,但它们提供了无视数据
量大小的快速访问——只要字典能被装在内存里。

4. 键的次序取决于添加顺序

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

#将同样的数据以不同的顺序添加到3个字典里
# 世界人口数量前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的时候,数据元组的顺序是按照国家名字的英文拼写来决定的。
➍ 这些字典是相等的,因为它们所包含的数据是一样的。

3个字典的键的顺序是不一样的

d1: dict_keys([880, 1, 86, 55, 7, 234, 91, 92, 62, 81]) 
d2: dict_keys([880, 1, 91, 86, 81, 55, 234, 7, 92, 62]) 
d3: dict_keys([880, 81, 1, 86, 55, 7, 234, 91, 92, 62])

5. 往字典里添加新键可能会改变已有键的顺序
无论何时往字典里添加新的键,Python解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。

set的实现以及导致的结果

set和frozenset的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章