字典与集合高级用法

2018-12-24  本文已影响0人  Zoulf

dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。模块的命名空间实例的属性和函数的关键字参数中都可以看到字典的身影。

第三章 字典和集合

可散列的数据类型:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象还需要实现__hash__()方法,另外可散列对象还需要有__eq__()方法,这样才能跟其他键做比较。

字典提供了很多种构造方法:

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

>>> a == b == c == d == e
True 

除了这些字面语法和灵活的构造元素以外,字典推导也可以用来建造新的 dict:

a = ['s', 'o', 's']
b = {k: v for k, v in enumerate(a)}

>>> print(b)
{0: 's', 1: 'o', 2: 's'}

使用 setdefault 来处理找不到的键

当字典 d[k] 不能找到正确的键时, Python 会抛出异常,这个行为符合 Python 所信奉的”快速失败“哲学,我们都知道可以用 d.get(k, default)来代替 d[k],给找不到的键一个默认的返回值。但是要更新某个键对应的值时,不管使用 __getitem__ 还是 get 都会不太自然,而且效率低。就像如下还没有经过优化的代码所显示的那样,dict.get 并不是处理找不到键的最好方式。

# -*- coding: utf-8 -*-
"""
@Desc    : 获取一段文本中的单词出现的频率以及出现的位置,如(1, 2), 横纵坐标都以 1 开始
"""

import sys
import re


# 创建正则 Pattern 对象,确保进行正则操作时不用重复创建 Pattern 对象
WORD_RE = re.compile(r'\w+')

index = {}

# sys.argv 是一个列表,sys.argv[0] 一般是"被调用的脚本名或全路径",sys.argv[1] 以及以后的都是传入的参数
# 如 python main.py ../../data/zen.txt
# sys.argv[0] 就是 main.py
# sys.argv[1] 就是 ../../data/zen.txt
with open(sys.argv[1], encoding='utf-8') as fp:
    # enumerate 有两个参数,第一个参数是一个 Iterable(可迭代对象),第二个参数 start 是开始位置,默认为 0
    for line_no, line in enumerate(fp, 1):
        # re.finditer(pattern, str) <=> pattern.finditer(str),返回一个iterator(迭代器)
        for match in WORD_RE.finditer(line):
            word = match.group()
            column_no = match.start() + 1
            location = (line_no, column_no)              
            # 提取 word 出现的情况,如果还没有该记录,返回[], 这不是一种好的写法
            occurrences = index.get(word, [])            # ①
            # 将新单词出现的位置添加到列表的后面
            occurrences.append(location)                 # ②
            # 将新列表放回到字典中,这里又牵扯到一次查询操作
            index[word] = occurrences                    # ③

# sorted 函数的 key=参数没有调用str.upper,而是把这个方法的引用传递给 sorted 函数
# 这样在排序的时候,就会按照str.upper处理后的方式进行排序。
for word in sorted(index, key=str.upper):
    print(word, len(index[word]))

上述处理单词的三行(① ② ③),通过 dict.setdefalut 可以只有一行就解决。

...
location = (line_no, column_no)
# 获取单词的出现情况列表,如果单词不存在,把单词和一个空列表放进映射,
# 然后返回这个空列表,这样就能在不进行第二次查找的情况下更新列表了
index.setdefault(word, []).append(location)
...

即:

my_dict.setdefault(key, []).append(new_value)

<=>

if key not in my_dict:
    my_dict[key] = []
my_dict[key].append(new_value)

只不过后者至少要进行两次键查询,如果键不存在的话,就是三次,用 setdefault 只需要一次就可以完成整个操作,以上是针对通过查找来插入新值时的最佳解决方式

字典的变种

创建自定义映射类型

就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。dict 会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法。

UserDict 并不是 dict 的子类,但是 UserDict 有一个叫作 data 的属性,是 dict 的实例,这个属性实际上是 UserDict 最终存储数据的地方。

'''
自定义字符串键字典
无论是添加,更新还是查询操作,StrKey都会把非字符串的键转换为字符串
'''
import collections


class StrKeyDict(collections.UserDict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]
    
    def __contains__(self, key):
        # 可以放心假设所有已经存储的键都是字符串,所以只要在self.data中查询就好了
        return str(key) in self.data
    
    def __setitem__(self, key, item):
        # self.data 为 dict 的实例
        self.data[str(key)] = item

字典中的散列表

散列表:其实是一个稀疏数组(总有空白元素的数组称为稀疏数组)。一般将散列表里的单元称为表元,在 dict 的散列表中,每个键值对都占用一个表元,每个表元有两个部分,分别是是对键、对值的引用,因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。

因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达到这个阀值的时候,原有的散列表会复制到一个更大的空间里。

字典的限制

  1. 键必须是可散列的
  2. 字典在内存上的开销巨大,由于散列表必须是稀疏的,这导致字典在空间上的效率低下,所以,如果你要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是比较好的选择。
  3. 往字典里添加新键可能会改变已有键的顺序。无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把已有的元素添加到新表里,这个过程可能会发生新的散列冲突,导致新建列表中键的次序变化。如果你在迭代一个字典的所有键的过程中同时对字典进行修改,那么这个循环很有可能会跳过一些键——甚至是跳过那些字典中已经有的键。

集合

集合字面量,{1}, {1, 2},但是如果是空集,必须写成set()的形式。

集合推导,{ i for i in range(10)}

集合可用于去重。

集合的特点:

上一篇 下一篇

猜你喜欢

热点阅读