第三章:字典与集合

dict类型在python中使用频率极高,与其有关的内置函数都在__builtins__.dict__模块中。

集合set的实现也依赖于散列表,因此本章中也会讲到它。反过来说,想要进一步理解集合和字典,就得先理解散列表的原理。

3.1 泛映射类型

colletions.abc模块中有Mapping 和MutableMapping 这两个抽象基类,他们的作用是dict 和其他类型定义形式接口


1

如图1 collections.abc 中的MutableMapping 和它的超类的UML图(箭头从子类指向超类,抽象类和抽象方法的名称以斜体显示)

然而,非抽象映射类型一般不会直接继承这些抽象基类,它们会直接对dict或者是colletions.User.Dict 进行扩展。这些抽象基类的主要作用是作为形式化的文档,它们定义了构建一个映射类型所需要的最基本的接口。然后它们还可以跟isinstance一起被用来判定某个数据是不是广义上的映射类型

什么是可散列的数据类型

关于可散 列类型的定义有这样一段话:
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变 的,而且这个对象需要实现__hash__() 方法。另外可散列对象还要有__qe__() 方法, 这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的 散列值一定是一样的……
原子不可变数据类型(str、bytes 和数值类型)都是可散列类型,frozenset 也是可散 列的,因为根据其定义,frozenset 里只能容纳可散列类型。元组的话,只有当一个元 组包含的所有元素都是可散列类型的情况下,它才是可散列的。

字典的构造方式

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([('two',2) , ('one',1) , ('three' , 3)])
e = dict({'three':3 ,'one':1 , 'two':2})
a == b == c == d == e
2

除了上述的句法,字典推导(dict comprehension)式也可以构建新dict

3.2字典推导

  • 字典推导的应用
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
3
{code :country.upper() for country,code in country_code.items() if code < 66}
4

3.3 常见的映射方法

映射类型的方法其实很丰富,下表为什么展示了dict defaultdict 和 OrderedDict 的常见方法,后面两个数据类型是dict的变种,位于collections 模块内

表:dict collections.defaultdict 和colletions.OrderedDict 这三种映射类型的方法列表(依然省略了继承自object的常见方法);可选参数以[...]表示

5
6

*default_factory并不是一个方法,而是一个可调用对象(callable),它的值在defaultdict初始化的时候由用户设定。

*OrderedDict.popitem() 会移除字典里最先插入的元素(先进先出);同时这个方法还有一个可选的last参数,若为真,则会移除最后插入的元素(后进先出)

上面的表格中,update 方法处理参数 m 的方式,是典型的“鸭子类型”。函数首先检查 m 是否有 keys 方法,如果有,那么 update 函数就把它当作映射对象来处理。否则,函数会 退一步,转而把 m 当作包含了键值对 (key, value) 元素的迭代器。Python 里大多数映射类 型的构造方法都采用了类似的逻辑,因此你既可以用一个映射对象来新建一个映射对象, 也可以用包含 (key, value) 元素的可迭代对象来初始化一个映射对象。
在映射对象的方法里,setdefault 可能是比较微妙的一个。我们虽然并不会每次都用它, 但是一旦它发挥作用,就可以节省不少次键查询,从而让程序更高效。如果你对它还不熟 悉,下面我会通过一个实例来讲解它的用法。

用setdefault处理找不到的键

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

  • 从索引中获取单次出现的频率信息,并把它们写进对应的列表里

"""创建一个从单次到其出现情况的映射"""

import os 
import re 

WORD_RE = re.compile(r'\w+')

index =   {}
with open(sys.argv[1] , encoding = 'utf-8') as fp:
    for line_no , line in enumerate(fp,1):
        word = match.group()
        column_no = match.start() +1
        location = (line_no , column_no)
        # 这其实是一种很不好的实现,这样写只是为了证明论点
        occurrences = index.get(word , [])
        occurrences.append(location)
        index[word] = occurrences

# 以字母顺序打印出结果
for word in sorted(index , key = str.upper):
    print(word , index[word])

3.4 映射的弹性键查询

有时候为了方便起见,就算某个键在映射里不存在,我们也希望在通过这个键读取值的时候能得到一个默认值。在两个途径能帮我们达到这个目的的,一个是通过defaultdict这个类型而不是普通的dict,另一个是给自己一个dict的子类,然后在子类中实现__missing__方法。下面将介绍这两种方法:

3.4.1 defaultdict:处理找不到的键的一个选择

具体而言,在实例化一个defaultdict的时候,需要给构造方法提供一个调用对象,这个可调用的对象会在__getitem__碰到找不到的键的时候被调用,让__getitem__返回某种默认值。

比如,我们新建了这样一个字典:dd = defaultdict(list) , 如果键'new-key'在dd中还不存在的话,表达式dd['new-key']会按照以下的步骤来行事。

(1)调用list()来建立一个新列表
(2)把这个新列表作为值,'new-key'作为它的键,放到dd中
(3)返回这个列表的引用
而这个用来生成默认值的可调用对象存放在名为default_factory的实例属性里。

  • 创建一个从单词到其出现情况的映射
import sys 
import re
import collections

index = collections.defaultdict(list) 
with open(sys.argv[1], encoding='utf-8') as fp:     
    for line_no, line in enumerate(fp, 1):         
        for match in WORD_RE.finditer(line):             
            word = match.group()             
            column_no = match.start()+1             
            location = (line_no, column_no) 
            index[word].append(location)   
for word in sorted(index, key=str.upper):     
  print(word, index[word])

3.4.2 特殊方法 __missing__

当有非字符串的键被查找的时候,StrKeyDict0 是如何在该键不存在的情况下,把它转换为字符串的

d = StrKeyDict0([('2' , 'two') , ('4' , 'four')])
d
d[4]
d[1]
d.get('2')
d.get(4)
d.get(1 , 'N/A')
2 in d
1 in d

如果要自定义一个映射类型,更合适的策略其实是继承collections.UserDict类。这里我们从dict继承,只是为了演示__missing__是如何被dict.__getitem__调用的。

#StrKeyDict0 继承了dict
class StrKeyDict0(dict):
    
    def __missing__(self , key):
        #如果找不到的键本身就是字符串,就抛出KeyError异常
        if isinstance(key , str):
            raise KeyError(key)
        #如果找不到的键不是字符串,那么把它转换成字符串再进行查找
        return self[str(key)]
    #get 方法把查找工作用 self[key] 的形式委托给 __getitem__, 这样在宣布查找失败之 前,还能通过 __missing__ 再给某个键一个机会。 
    def get(self , key , default = None):
        try:
            return self[key]
    #如果抛出 KeyError,那么说明 __missing__ 也失败了,于是返回 default
        except KeyError:
            return default
  #先按照传入键的原本的值来查找(我们的映射类型中可能含有非字符串的键),如果没 找到,再用 str() 方法把键转换成字符串再查找一次。

    def __contains__(self , key):
        return key in self.keys() or str(key) in self.key()

3.5 字典的变种

这一节总结了标准库里 collections 模块中,除了 defaultdict 之外的不同映射类型。

collections.OrderedDict
这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像my_odict. popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。

collections.ChainMap
该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当 作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解 释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。

collections.Counter
这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数 器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合 就是集合里的元素可以出现不止一次。Counter 实现了 + 和 - 运算符用来合并记录,还 有像 most_common([n]) 这类很有用的方法。most_common([n]) 会按照次序返回映射里最 常见的n 个键和它们的计数,下面的小例子利用 Counter 来计算单词中各个字母出现的次数:

import collections
ct = collections.Counter('abracadabra')
ct
7
ct.update('aaaaazzz')
ct
8
ct.most_common(2)
9

3.6 子类化UserDict

就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。
这体现在,我们能够改进示例 3-7 中定义的 StrKeyDict0 类,使得所有的键都存储为字符 串类型。
而更倾向于从 UserDict 而不是从 dict 继承的主要原因是,后者有时会在某些方法的实现 上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这 些问题。

  • 无论是添加、更新还是查询操作 , StrKeyDict 都会把非字符串的键转换为字符串
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):
        return str(key) in self.data

    def __setitem__(self , key , item):
        self.data[str(key)] = item

因为 UserDict 继承的是 MutableMapping,所以 StrKeyDict 里剩下的那些映射类型的方法 都是从 UserDict、MutableMapping 和 Mapping 这些超类继承而来的。特别是最后的 Mapping 类,它虽然是一个抽象基类(ABC),但它却提供了好几个实用的方法。以下两个方法值 得关注。

  • MutableMapping.update
    这个方法不但可以为我们所直接利用,它还用在__init__ 里,让构造方法可以利用传入的各种参数(其他映射类型、元素是 (key, value) 对的可迭代对象和键值参数)来 新建实例。因为这个方法在背后是用 self[key] = value 来添加新值的,所以它其实是 在使用我们的 __setitem__ 方法。

  • Mapping.get
    为它继承了 Mapping.get 方法, 这个方法的实现方式跟 StrKeyDict0.get 是一模一样的。

3.7 不可变映射类型

从 Python 3.3 开始,types 模块中引入了一个封装类名叫 MappingProxyType。如果给这个类 一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味 着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原 映射做出修改。

from types import MappingProxyType
d = {1:'A'}
d_proxy = MappingProxyType(d)
d_proxy
10

3.8 集合论

集合指set ,也指frozenset。

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

l = ['spam' , 'spam' , 'eggs' , 'spam']
set(l)
11

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

除了保证唯一性,集合还实现了很多基础的中缀运算符 。给定两个集合 a 和 b,a | b 返 回的是它们的合集,a & b 得到的是交集,而 a - b 得到的是差集。合理地利用这些操作, 不仅能够让代码的行数变少,还能减少 Python 程序的运行时间。这样做同时也是为了让代 码更易读,从而更容易判断程序的正确性,因为利用这些运算符可以省去不必要的循环和 逻辑操作。
例如,我们有一个电子邮件地址的集合(haystack),还要维护一个较小的电子邮件地址集 合(needles),然后求出 needles 中有多少地址同时也出现在了 heystack 里。借助集合操 作,我们只需要一行代码就可以了

found = len(needles & haystack)

needles 的元素在 haystack 里出现的次数

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

3.8.1 集合字面量

除空集之外,集合的字面量——{1}、{1, 2},等等——看起来跟它的数学形式一模一样。 如果是空集,那么必须写成 set() 的形式。
不要忘了,如果要创建一个空集,你必须用不带任何参数的构造方法 set()。 如果只是写成 {} 的形式,跟以前一样,你创建的其实是个空字典。

3.8.2 集合推导

from unicodedata import name
{chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')}
11

3.8.3 集合的操作

3.9 dict 和 set 的背后

想要理解 Python 里字典和集合类型的长处和弱点,它们背后的散列表是绕不开的一环。
这一节将会回答以下几个问题。

  • Python 里的 dict 和 set 的效率有多高?
  • 为什么它们是无序的?
  • 为什么并不是所有的 Python 对象都可以当作 dict 的键或 set 里的元素?
  • 为什么 dict 的键和 set 元素的顺序是跟据它们被添加的次序而定的,以及为什么在映射对象的生命周期中,这个顺序并不是一成不变的?
  • 什么不应该在迭代循环 dict 或是 set 的同时往里添加元素?
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,585评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,923评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,314评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,346评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,718评论 3 291
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,828评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,020评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,758评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,486评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,722评论 2 251
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,196评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,546评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,211评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,132评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,916评论 0 200
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,904评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,758评论 2 274

推荐阅读更多精彩内容