流畅的python:字典

第三章 字典与集合

字典这个数据结构活跃在所有Python程序的背后,即便你的源码里并没有直接用到它。

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

3.1 泛映射类型

  1. dict的祖先与后代

collections.abc模块中有Mapping和MutableMapping这两个抽象基类,定义了构建一个映射类型所需要的最基本的接口,下图是一些常用的方法:

在这里插入图片描述

内置数据类型dict和其他类似的类型均使用这些接口:

from collections import abc
my_dict = {}
isinstance(my_dict, abc.Mapping)
# True

不过一般的的映射类型不会直接继承这两个抽象基类,而是依托于dict或是collections.UserDict进行扩展。例如标准库里的所有映射类型都是利用dict来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键。

  1. 可散列数据

可能当你学字典的时候,就知道键必须是不可变的,如字符串,数字或元组。事实上,这个说法并不准确,准确来说,键必须是可散列数据。可散列数据的定义为:

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()方法。

换句话说,这个对象必须是可哈希的:原子不可变数据类型(str、bytes和数值类型)都可哈希,都是可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的,具体原因可以参见我的另一篇博文:元组的相对不可变性

>>> hash(2)
2
>>> hash('fr')
7645933407608567419
>>> hash((2,3))
3713082714463740756
>>> hash((2,[3,4]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

另外可散列对象还要有__eq__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……

一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们的id( )函数的返回值,所以所有这些对象在比较的时候都是不相等的。

3.2 字典推导

字典的创建方式太多了,下面这些方式都可以创建字典:

>>> 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})

我们在这里讲一种新的创建方式字典推导,可以从任何以键值对作为元素的可迭代对象中构建出字典。

keys = 'one two three'.split(' ')
values = range(1, 4)
f = {key: value for key,value in zip(keys,values)}

如果你会使用列表推导式,上面的例子很容易理解,如果现在还不知道这个功能,我想是时候掌握他了!

3.3 常见的映射方法

映射类型有很多,不过像前面所说的,大多数都依靠dict扩展,比如collections模块内的defaultdict和OrderedDict。所以大多数的方法都是大同小异,就像d.items()获取多有键值对,几乎是所有影射对象共有的,再比如in操作符所对应的魔法方法__contains__。大多数方法相信大家在初学python中都知道,在这里我们额外介绍几个常用方法:

  1. 用get和setdefault处理找不到的键

当字典d[k]不能找到正确的键的时候,Python会抛出KeyError异常,所以几乎每个Python程序员都知道可以用d.get(k, default)来代替d[k],给找不到的键一个默认的返回值:

d = dict(one=1, two=2, three=3)
d.get('four','N/A')

这样很好,但是如果我们要更新某个键对应的值的时候,你可能会想到用:

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

我在找工作笔试的时候,不止一次用过这个代码,注意,我们这里所说的是增删的更新,而不是修改,如果是修改的话,你大可以直接使用my_dict[key] = new_value直接进行。

上面的代码看起来并没有什么问题,直到我看了这本书,我才发现更为优雅的做法是像下面这样:

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

尽管两者的效果是一样的,但是前者至少要进行两次键查询(如果键不存在的话,就是三次),用setdefault只需要一次就可以完成整个操作。

也就是说,大家如果只想获取某个key对应的value,可以使用d.get(k, default);如果只想修改,不关注其存在不存在,直接使用my_dict[key] = new_value;如果只想在键存在的情况进行增删的更新,推荐使用setdefault(key, [])方法。

  1. __missing__(了解)

所有的映射类型在处理找不到的键的时候,都会牵扯到__missing__方法。只有在__getitem__碰到找不到的键的时候,Python就会自动调用它,而不是抛出一个KeyError异常。不过大多数时候我们并用不到,我们只用一个例子简单地说明。

# 当有非字符串的键被查找的时候,Mydict将该键转换为字符串再次尝试
class Mydict(dict):
    def __missing__(self, key):
        # 如果是个整数,就再给一次机会,否则就报错
        if isinstance(key, int):
            return self[str(key)]
        else:
            raise KeyError

    def get(self, key):
        try:
            return self[key]
        except KeyError:
            return None

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


d = Mydict(zip(['1', '2', '4'], 'one two four'.split(' ')))
# {'1': 'one', '2': 'two', '4': 'four'}
>>>2 in d
True
>>>d[4]
'four'

在上面的例子中,我们使用get方法把查找工作用self[key]的形式委托给__getitem__,这样在宣布查找失败之前,还能通过__missing__再给某个键一个机会。比如说d[4],当未查询到是时,__getitem__会调用__missing__方法,再次进行查找,最终返回d[‘4’]。是我们从dict继承到的__contains__方法不会在找不到键的时候调用__missing__方法,在这里列出来只是为了保证程序的一致性。

3.4 字典的变种

collections标准库里面还有很多其他的映射类型,我们现在大致看一下,不是很常用到,仅作了解

类型介绍
collections.OrderedDict在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict的popitem方法默认删除并返回的是字典里的最后一个元素,但是如果像my_odict.popitem(last=False)这样调用它,那么它删除并返回第一个被添加进去的元素。
collections.ChainMap可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。
collections.Counter这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。Counter实现了+和-运算符用来合并记录,还有像most_common([n])这类很有用的方法。most_common([n])会按照次序返回映射里最常见的n个键和它们的计数。
collections.UserDict这个类其实就是把标准dict用纯Python又实现了一遍,是专门让用户继承写子类的。更倾向于从UserDict而不是从dict继承的主要原因是,dict有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方
  • collections.Counter
import collections

# 对可散列对象计数
ct = collections.Counter('aasddwwddafgsgdfsg')
>>>ct
Counter({'a': 3, 's': 3, 'd': 5, 'w': 2, 'f': 2, 'g': 3})
# 增加对象
ct.update('aaassffff')
>>>ct
Counter({'a': 6, 's': 5, 'd': 5, 'w': 2, 'f': 6, 'g': 3})
# 最多的n个键
ct.most_common(2)
[('a', 6), ('f', 6)]

——未完待续——
欢迎关注我的微信公众号
扫码关注公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值