字典推导
列表推导和生成器表达式的概念就移植到了字典上,从而有了字典推导(后面还会看到集合推导)。
字典推导(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的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。
• 集合里的元素必须是可散列的。
• 集合很消耗内存。
• 可以很高效地判断元素是否存在于某个集合。
• 元素的次序取决于被添加到集合里的次序。
• 往集合里添加元素,可能会改变集合里已有元素的次序。