字典及散列表,字典用法

字典和散列表

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

正是因为字典至关重要,Python对它实现了高度优化,而散列表则是字典类型性能出众的根本原因。

collections.abc模块中有MappingMutableMapping这两个抽象基类,它们的作用是为dict和其他类似的类型定义形式接口(在Python2.6到Python3.2的版本中,这些类还不属于collections.abc模块,而是隶属于collections模块。)

这里写图片描述

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

>>> isinstance(my_dict,Mapping)
True

注:在Python3.8以后,ABCs将会替代abc

>>> from collections import Mapping
__main__:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working

这里用instance而不是type来检查某个参数是否为dict类型,因为这个参数可能不是dict,而是一个比较另类的映射类型。

目前,标准库里所有的映射类型都是利用dict来实现的,因此他们有个共同的限制,即只有可散列的数据类型才能用作这些映射的键(只有键有这个要求,值并不需要是可散列的数据类型)。
注:什么是可散列的数据类型?

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

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

在Python中,list是不可散列的,所以它不能作为字典的键。为什么这样说呢?

首先我们来看看字典的工作原理:

字典就是将其映射到上,为了实现这个功能,Python需要做到通过给出的key精确的找到value。那么怎么实现这个功能呢?

首先考虑一种简单的算法,将每一个键值放在一个列表里面,每次去拿键的时候都要遍历所有的列表来得到keyvalue,然后通过对比key来得到value。这种算法在数据量较小的时候还尚可,但是我们都知道Python的执行效率是比较低下的,所以在数据量变大的时候该算法效率就会变得无法忍受。所以Python采用了hash算法来做这样的事情。

Python要求每一个存放在字典中的对象都必须是可散列的,都能实现hash函数,通过hash函数可以得到一个哈希值,通过这个哈希值,Python可以快速确定对象在字典中的位置。不过仅仅是通过key来确定位置是不准确的,因为不同的key可能会产生相同的哈希值(哈希冲突),所以校验value也是有必要的。所以字典的查询过程大概分为以下三步:

  1. 生成key的哈希值。
  2. 通过哈希值来确定对象的位置,这里确定的位置是存放着对象的数组,当然,理想情况下这个数组里面只有一个对象。
  3. 遍历这个数组,找到目标key,返回对应的value

这里如果两个value的哈希值是不同的,那么他们key的哈希值也必然是不同的,否则对象相同,value却不同,会导致数据结构混乱。相反,如果两个key-vaue是相等的,那么这两个对象也是相等的,这样做是为了尽量保证每个数组里面只有一个元素,提高执行效率。

那么回到最初的话题,为什么list是不能作为字典的键的?

因为字典是不可以被哈希的,如下:

>>> a = [3,4]
>>> id(a)
4501650312
>>> hash(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

那么为什么list不能被哈希呢?首先list是可变的,如果我们对list做修改,那么它原来存放位置的哈希值就不能再用了。其次,因为hash是基于id的,但是相同的list可能会产生不同的id,所以字典仍然会把相同值的key-value当做不同的对象,这就不满足上面的条件。

由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例而言,如果你需要存放数量巨大的记录,那么放在由元祖或是具名元祖构成的列表中会是比较好的选择;最好不要根据JSON的风格,用由字典组成的列表来存放这些记录。用元祖取代字典就能节省空间的原因有两个:其一是避免了散列表消耗的空间,其二是无需把记录中字段的名字在每个元素里都存一遍。

不要对字典同时进行迭代和修改。如果想扫描并修改一个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。因为无论何时往字典里添加新的键,Python解释器都可能做出为字典扩容的决定,扩容会新建一个更大的散列表,并数据迁移,这个过程中可能发生新的散列冲突,导致新散列表中键的次序发生变化。如果在迭代字典的键的过程中同时进行修改,这个循环可能会跳过一些键–甚至是跳过那些字典中已有的键。

常见的几种字典生成方法:

>>> 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
True

字典类型

collections.OrderedDict

Return an instance of a dict subclass, supporting the usual dict methods. An OrderedDict is a dict that remembers the order that keys were first inserted. If a new entry overwrites an existing entry, the original insertion position is left unchanged. Deleting an entry and reinserting it will move it to the end.
Python官方文档

OrderedDict是一个记住键首次插入的顺序的字典,返回一个dict子类的实例,支持通常的dict方法。 如果新条目覆盖现有条目,则原始插入位置保持不变。 删除条目并重新插入它会将其移至最后。
不过个人认为现在这个类型变得有点鸡肋,因为Python3.6对字典实现了改写,现在的字典是一个有序的数据结构,新字典dict() 的内存占用比Python3.5中减少20%到25%,不过这一切在未来可能会变化。

collections.defaultdict

defaultdict是字典中找不到key时候的一种选择,应用场景主要是单纯的取值操作。

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

比如,我们新建额这样一个字典:dd=defaultdict(list),如果键new_keydd中不存在的话,表达式dd['new_key']会按照以下的步骤来行事:
1. 调用list()来创建一个新列表。
2. 把这个新列表作为值,new_key作为键,放到dd中。
3. 返回这个列表的引用。

而这个用来生成默认值的可调用对象存放在名为default_factory的实例属性里。

>>> from collections import defaultdict
>>> dd = defaultdict(list)
>>> dd['a']= 1
>>> dd['b'] = 2 
>>> dd['c'] = 3
>>> dd['d']
[]
>>> dd
defaultdict(<class 'list'>, {'a': 1, 'b': 2, 'c': 3, 'd': []})

如果在创建defaultdict的时候没有指定default_factory,查询不存在的键会触发KeyError。另外,defaultdict里的default_factory只会在__getitem__里被调用,在其他地方完全不会发挥作用。比如,dd[key]会调用这个方法,而dd.get(key)则会返回None

最后,我们都知道可以使用dd.get(key,value)代替dd[key],给找不到的键一个默认的返回值,但是要更新某个键对应的值得时候,不管使用__getitem__还是get都会不自然,而且效率低下,Python提供了一个seatdefault的方法。

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

等效于

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

二者的效果是一样的,只不过后者至少要进行两次键查询–如果键不存在的话,就是三次,用seatfault只需要一次就可以完成整个操作,它更适用于通过查找键来插入新值。

defaultdict和setdefault的区别:如果查询的键不存在default会创建一个新的列表并放入dict中,而setdefault只会在查询这个键的时候才会调用。

collection.defaultdict(list) ——>在创建字典的时候定义。

my_dict.setdefault(key,[]) ——>在查询这个键的时候定义,一般使用这种

dictdefaultdictOrderedDict
d.clear()...移除所有元素
d.contains(k)...检查k是否在d中
d.copy()...浅复制
d.copy().用于支持copy.copy
d.default_factory.__missing__函数中被调用的函数,用以给未找到的元素设置值
d.delitem(k)...del d[k],移除键为k的元素
d.fromkeys(it, [initial])...将迭代器it里的元素设置为映射里的键,如果有initial参数,就把它作为这些键对应的值(默认是None)
d.get(k, [default])...返回键k对应的值,如果字典里没有键k,则返回None或者default
d.getitem(k)...让字典d能用d[k]的形式返回k对应的值
d.items()...返回d里所有的键值对
d.iter()...返回键的迭代器
d.keys()...返回所有的键
d.len()...可以用len(d)的形式得到字典里键值对的数量
d.missing(k).getitem找不到对应键的时候,这个方法会被调用
d.move_to_end(k, [last]).把键为k的元素移动到最靠前或者最靠后的位置(last的默认值是True)
d.pop(k,[default])...返回键k所对应的值,然后移除这个键值对。如果没有这个键,返回None或者default
d.popitem()...随机返回一个键值对并从字典里移除它
d.reversed().返回倒序的键的迭代器
d.setdefault(k, [default])...若字典里有键k,则把它对应的值设为default,然后返回这个值;若无,则让d[k]=default,然后返回default
d.setitem(k, v)...实现d[k]=v操作,把k对应的值设为v
d.update(m, [**kargs])...m可以是映射或者键值对迭代器,用来更新d里对应的条目
d.value()...返回所有的值

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值