学习dict对象使用技巧(Python)

Last Updated: 2024-05-08 07:56:44 Wednesday

-- TOC --

本文总结一些Python内置dict对象的使用方法和技巧,遇到了就记录一点,常读常新!

Hashable Key

Python中的一切都是对象,那么哪些数据对象可以用来作为dict对象中数据的key呢?

答:Hashable对象!

Python内置的immutable对象都可以作为key,它们主要有string,number(int,float)和tuple(仅含有immutable对象的tuple对象),bytes,以及frozenset。用户定义的class,默认是hashable的,解释器使用id()的返回值来计算hash code,用户也可以自己定义__hash____eq__这两个接口。

示例:

>>> d3 = {1.23:1, 2.34:2}        # key: float object
>>> d3
{1.23: 1, 2.34: 2}
>>> d4 = {b'\x01':1, b'\x02':2}  # key: bytes object
>>> d4
{b'\x01': 1, b'\x02': 2}
>>> d5 = {(1,2):1, (2,3):2}      # key: tuple object
>>> d5
{(1, 2): 1, (2, 3): 2}

包含list的tuple属于unhashable

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

如何删除Key

一般使用pop成员函数接口,这个接口可以实现无raise的效果,即当调用时提供default value时,如果key不存在,返回default value,不会raise:

>>> d = {'a':1, 'b':2}
>>> d.pop('a',None)
1
>>> d.pop('a',None)
>>>

默默删除,确保传入的key被删除或不存在,这个特性非常Nice。

也可以用del语句来删除某个key,但当key不存在的时候,会raise KeyError:

>>> d = {'a':1, 'b':2}
>>> del d['a']
>>> del d['a']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'a'

另外,popitem也可用来删除,按插入数据的相反的顺序,当dict对象为空时,会raise KeyError。

可依赖的有序性

从Python3.6开始,dict对象会记录数据插入的顺序,这个顺序就是遍历顺序。

>>> d5 = {}
>>> d5['a'] = 1
>>> d5['c'] = 3
>>> d5['e'] = 5
>>> d5['b'] = 2
>>> d5['d'] = 4
>>> for k,v in d5.items():
...     print(k,v)
...
a 1
c 3
e 5
b 2
d 4
>>> list(d5)
['a', 'c', 'e', 'b', 'd']
>>> tuple(d5)
('a', 'c', 'e', 'b', 'd')

如果使用popitem接口,就会按插入倒叙的方式弹出!

比较dict对象

两个dict对象,只要所有的key和value相等,不管他们的插入顺序如何,两个dict对象就是相等的:

>>> d1 = {}
>>> d1['a'] = 1
>>> d1['b'] = 2
>>>
>>> d2 = {}
>>> d2['b'] = 2
>>> d2['a'] = 1
>>>
>>> d1 == d2
True

虽然dict对象有插入顺序,但比较的时候并不关心这个顺序,只关心key/value。

合并dict对象

Python从3.9开始提供了两个新的合并dict对象的操作符,||=

>>> a = {'a':1, 'b':2}
>>> b = {'b':3, 'c':4}
>>> {**a, **b}
{'a': 1, 'b': 3, 'c': 4}
>>> a | b
{'a': 1, 'b': 3, 'c': 4}
>>>
>>> {**b, **a}
{'b': 2, 'c': 4, 'a': 1}
>>> b | a
{'b': 2, 'c': 4, 'a': 1}

注意顺序,从左到右!

update方法对应的是 |= 操作符:

>>> a
{'a': 1, 'b': 2}
>>> b
{'b': 3, 'c': 4}
>>> a.update(b)
>>> a
{'a': 1, 'b': 3, 'c': 4}
>>>
>>> a = {'a':1, 'b':2}
>>> b
{'b': 3, 'c': 4}
>>> a |= b
>>> a
{'a': 1, 'b': 3, 'c': 4}

dict对象的key不可重复,遇到相同的key,value就会被更新。

模拟LIFO队列(Stack)

模拟 Last In First Out 队列,即stack数据结构,dict对象也可以,由于插入数据的有序性,其popitem成员接口可以一个个地按相反的顺序,弹出数据:

>>> d = {'a':1, 'b':2, 'c':3}
>>> d.popitem()
('c', 3)
>>> d.popitem()
('b', 2)
>>> d.popitem()
('a', 1)
>>> d.popitem()  # raise KeyError when empty
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'popitem(): dictionary is empty'

d定义的方法,从左到右,就是数据插入的方式。

优雅的setdefault

用这个接口更新某个key的value是很优雅的!特别是当key还不存在的时候,可以一行代码实现先赋默认值,然后更新的效果。

>>> d = {}
>>> d.setdefault('a',[]).append(1)
>>> d.setdefault('a',[]).append(2)
>>> d
{'a': [1, 2]}
>>> d.setdefault('b',[]).append(3)
>>> d.setdefault('b',[]).append(3)
>>> d
{'a': [1, 2], 'b': [3, 3]}

setdefault与get有点相似,但是不一样,前者设置默认值后,返回这个值的ref,后者在遇到key不存在的情况时,仅仅只是返回一个预设值,这个值与d无关。

dictcomp

List Comprehension太有名了,其实还有类似的dictcomp和setcomp。

>>> {i:(i,) for i in range(8) if i%2}
{1: (1,), 3: (3,), 5: (5,), 7: (7,)}

快速的in测试

dict和set的membership测试,显然比list和tuple要快!

$ python -m timeit -p -s 'a=[i for i in range(1000)]' '456 in a'
50000 loops, best of 5: 4.57 usec per loop
$ python -m timeit -p -s 'a={i:i for i in range(1000)}' '456 in a'
5000000 loops, best of 5: 44.3 nsec per loop
$ python -m timeit -p -s 'a={i for i in range(1000)}' '456 in a'
10000000 loops, best of 5: 39 nsec per loop

快了差不多100倍!

Delete while Loop

在遍历的过程中删除,这个操作错误的,解释器会抛出RuntimeError: dictionary changed size during iteration。正确的做法,是遍历一个copy,或者在遍历时创建新的dict对象。

>>> d = {str(i):i for i in range(100)}
>>> for k in d.copy():
...     if int(k) > 1:
...         d.pop(k)
...

copy接口实现的是shallow copy。(list对象也有copy接口,不过我们更喜欢使用[:]这个slice操作)

Counter对象

dict是hashtable,从key到value。set只有key,没有value。Counter也是hashtable,有key,但value为key出现的次数!

>>> from collections import Counter
>>> a = Counter("abcde12345abcde5678999999")
>>> a
Counter({'9': 6, 'a': 2, 'b': 2, 'c': 2, 'd': 2, 'e': 2, '5': 2, '1': 1, '2': 1, '3': 1, '4': 1, '6': 1, '7': 1, '8': 1})
>>> a.most_common()
[('9', 6), ('a', 2), ('b', 2), ('c', 2), ('d', 2), ('e', 2), ('5', 2), ('1', 1), ('2', 1), ('3', 1), ('4', 1), ('6', 1), ('7', 1), ('8', 1)]

对于不存在的key,Counter不会raise KeyError,只是返回0,表示出现频次为0,即没有这个key。这个技巧还蛮重要的,在需要Counter做统计的时候,代码可以写得很优雅:

>>> a['x']
0
>>> a['y'] += 1  # Key 'y' is not existed
>>> a['y']
1

set常常用来去重,Counter中的key也实现了去重效果,并且还统计的频次!

>>> a = Counter("221333")
>>> for k in a.elements():
...   print(k)
...
2
2
1
3
3
3
>>> a.keys()
dict_keys(['2', '1', '3'])
>>> a = Counter(a=3,b=3)
>>> b = Counter(a=2,b=1)
>>> a + b
Counter({'a': 5, 'b': 4})
>>> a - b
Counter({'b': 2, 'a': 1})
>>> a & b
Counter({'a': 2, 'b': 1})
>>> a | b
Counter({'a': 3, 'b': 3})
>>> a.update(b)
>>> a
Counter({'a': 5, 'b': 4})
>>> a.subtract(b)
>>> a
Counter({'a': 3, 'b': 3})
>>> a = Counter(a=0,b=-1)
>>> a
Counter({'a': 0, 'b': -1})
>>> for k in a.elements():
...   print(k)
...
>>> a['b']
-1
>>> a['a']
0
>>> 'a' in a
True
>>> 'c' in a
False

我有一个应用Couter的case,LeetCode第30题,寻找包含所有words的子串

应用dict.get接口

dict.get接口与dict.setdefault有点类似,但又有所不同。后者设置初值或返回当前值;前者不设置初值,但可在没有初值的情况下给出一个默认的值返回。

下面这段代码,用dict模拟了Counter:

>>> a = ['a','a','b','c','d','c','a']
>>> from collections import Counter
>>> Counter(a)
Counter({'a': 3, 'c': 2, 'b': 1, 'd': 1})
>>>
>>> c = {}
>>> for it in a:
...   c[it] = c.get(it,0) + 1
...
>>> c
{'a': 3, 'b': 1, 'c': 2, 'd': 1}
>>> c == Counter(a)
True

本文链接:https://cs.pynote.net/sf/python/202206251/

-- EOF --

-- MORE --