字典与集合高级用法

dict 类型不但在各种程序里广泛使用,它也是 Python 语言的基石。??榈拿占涫道氖粜院秃墓丶植问卸伎梢钥吹阶值涞纳碛啊?/p>

第三章 字典和集合

可散列的数据类型:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象还需要实现__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 只需要一次就可以完成整个操作,以上是针对通过查找来插入新值时的最佳解决方式。

字典的变种

  • collections.OrderedDict, 有序字典,在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 默认删除并返回的是字典里的最后一个元素,但是如果 popitem(last=False) 则删除并返回第一个被添加进的元素。
  • collections.ChainMap,该类型可以容纳数个不同的映射对象,在进行键查找的时候,这些对象会被当做一个整体被逐步查找,直到键被站到位置。
  • collections.Counter,该类型会给键准备一个整数计算器,每次更新一个键的时候都会增加这个计数器。所以这个类型还可以用来给可散列对象计数。Counter 实现了 + 和 - 运算符用来合并记录,还有 most_common([n]),它会按照次序返回映射里最常见的 n 个键和他们的计数。
  • collections.UserDict,该类就是把标准的 dict 用纯 Python 又实现了一遍,跟上述这些开箱即用的类型不同, UserDict 是让用户继承写子类(自定义映射类型)的。

创建自定义映射类型

就创造自定义映射类型来说,以 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)}。

集合可用于去重。

集合的特点:

  • 集合里的元素必须是可散列的
  • 集合很消耗内存
  • 可以很高效的判断元素是否存在与某个集合
  • 元素的次序取决于被添加到集合里的次序
  • 往集合里添加元素,可能会改变集合里已有元素的次序
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容