collections.defaultdict
是 dict.setdefault()
基础上发展而来的。
首先回顾 dict
类型最基本的取值方法:方括号 []
__getitem__() 方法
只要在自定义类里定义了这个方法,那么实例化出来的对象就拥有了 []
取值的能力。
>>> class A:
... def __getitem__(self, index):
... return 'get item.'
...
>>> a=A()
>>> a[1]
'get item.'
>>> a['a']
'get item.'
>>>
如果是 dict
对象,用 []
访问的 key 不存在,就会直接抛出 KeyError
异常。
>>> d={'a':1}
>>> d['b']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'b'
>>>
为了更优雅、更方便的处理这种异常场景,发展出了 get() 和 setdefault() 函数
get() 和 setdefault()
get()
提供了返回默认值功能
setdefault()
在 get()
基础上,提供了将默认值(default)插回(set)字典的功能。这就是 setdefault
名称的由来。
>>> d={'a':1}
>>> d.setdefault('b',2)
2
>>> d
{'a': 1, 'b': 2}
>>>
但是每次调用 setdefault
函数,又不如 []
提供的方式优雅,所以希望在 []
取值方式中实现 setdefault
的功能。
# 想象中的样子
>>> d={'a':1}
>>> d['b'] # 不报错,直接创建默认值
>>> d
{'a': 1, 'b': 默认值}
于是需要改造 dict
类型,从 dict
派生出新类来实现这种需求。有下面两种方法:
- 自己编写 dict 的派生类
- 使用已有的
collections.defaultdict
这两种方法改造的关键点都是重(chóng)写 __missing__()
特殊方法。
__missing__() 方法
当基类 dict
发现给出的键不存在时,都会调用 __missing__()
方法。
虽然 dict
并没有定义这个方法,但是不妨碍它知道 __missing__()
这么个方法存在。
如果向子类的 __getitem__(key)
提供的 key
不存在的时候,就会自动的调用 __missing__()
方法,同时不会抛出 KeyError
异常。
如果在 __missing__()
方法中设置一个动作,即向自己插入一个默认值,就实现了 这一节 最末希望实现的样子。
这里插入的默认值,是用一个工厂方法 default_factory
实现的,由 __missing__()
方法调用。
default_factory
default_factory 是一个 callable 对象,可以是一个函数或者类,当 __getitem__(key)
的 key
不存在时,default_factory 会被 __missing__()
方法调用,用于生成那个不存在的 key
对应的默认值。
在 __missing__()
方法调用 default_factory
的时候,是不带任何参数的。
所以 default_factory
应该有以下这种行为:
>>> default_factory()
默认值
>>>
满足上述行为的,可以是以下这些对象:
- 自定义的函数
- 内置类型 int、list、str、set
- 自定义的类
理解了以上这些概念,就很好理解 collections.defaultdict
的用法了
defaultdict
collections.defaultdict
是内置字典类型 dict
的一个派生类,和 dict
类的区别在于:
- 重写了一个方法(__missing__())
- 增加了一个位置参数(default_factory)
其余使用方法与 dict
完全相同。增加的 default_factory
参数用于生成默认值。
defaultdict(default_factory=None, /[, ...])
default_factory
的理念和可以取的值在 这一节 中介绍。
例子
基本的初始化方法
defaultdict
在最开始位置增加了 default_factory
参数用于生成默认值:
首先导入模块
>>> from collections import defaultdict
构造一个函数用于返回默认值
>>> def d():
... return 'default'
...
实例化 defaultdict
>>> a=defaultdict(d)
>>> a
defaultdict(<function d at 0x000001BD3751DF70>, {})
访问一个不存在的key,这时自动生成 {1: 'default'}
的键值对
>>> a[1]
'default'
这一步的过程是:
- [1] 语法调用
__getitem__(1)
__getitem__(1)
发现不存在1
这个键__getitem__(1)
调用__missing__()
__missing__()
不带参数调用函数d()
,得到默认值'default'
__missing__()
将不存在的键1
和默认值'default'
组成键值对,插入到自身对象中
查看对象的变化
>>> a
defaultdict(<function d at 0x000001BD3751DF70>, {1: 'default'})
>>>
这时已经有了 {1: 'default'}
的记录。
这样就是达到了用 []
的语法实现 setdefault()
函数的目的。
其他形式的 default_factory
上一节 中介绍了自定义函数作为 default_factory
的例子,在 这一节 中提到,还有另外两种 default_factory
的形式:
- 内置类型 int、list、str、set
- 自定义的类
内置类型作为可调用对象(callable),返回的是空的默认值:
>>> int()
0
>>> str()
''
>>> list()
[]
>>> set()
set()
>>>
在一些统计的场合,可以直接用作 default_factory
这里举个来自标准库文档里归类的例子
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
d[k].append(v)
sorted(d.items())
# [('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
在第4行中,如果 d
中没有 k
,则新建一个 {k: []}
的记录,并且返回空列表 []
的引用,仿佛存在 k
一般。
所以,无论 d
中有没有 k
,都使得循环体能够持续执行。整个代码非常简洁、易读。
如果用 setdefault()
,第4行将成为:
d.setdefault(k, []).append(v)
工厂函数
如果像 这一节 中自定义函数用作 default_factory
,只能返回一个固定值。如果希望将这个固定值参数化,可以使用工厂函数的方法。
普通函数:
def d():
return 'default'
工厂函数:
def d(count):
def f():
return count
return f
工厂函数返回的是一个函数,这样无参数调用 d(count)
,实质上调用的是 f()
>>> a=defaultdict(d(10))
>>> a[1]
10
>>> a
defaultdict(<function d.<locals>.f at 0x1076e4550>, {1: 10})
>>>
工厂函数还能简单地写成匿名 lambda 函数:
def d(count):
return lambda: count
效果和上面的一致。
其他初始化方法
先看 dict
类型的用法,有三种初始化的方法:
# dict()
>>> dict()
{}
>>>
# dict(**kwarg)
>>> dict(one=1, two=2, three=3)
{'one': 1, 'two': 2, 'three': 3}
>>>
# dict(mapping, **kwarg)
>>> dict({'one': 1, 'two': 2, 'three': 3})
{'one': 1, 'two': 2, 'three': 3}
>>>
# dict(iterable, **kwarg)
>>> dict([('two', 2), ('one', 1), ('three', 3)])
{'two': 2, 'one': 1, 'three': 3}
>>>
既然 defaultdict
继承了 dict
的行为,那么上述 dict
的初始化方法也都适用。
>>> def d(count):
... return lambda: count
...
>>> a=defaultdict(d(4), one=1, two=2, three=3)
>>> a['four']
4
>>> list(a.items())
[('one', 1), ('two', 2), ('three', 3), ('four', 4)]
>>>
如果您对本文有疑问或者寻求合作,欢迎 联系邮箱 。邮箱已到剪贴板
精彩评论
本站 是个人网站,采用 署名协议 CC-BY-NC 授权。
欢迎转载,请保留原文链接 https://www.lfhacks.com/tech/python-defaultdict/ ,且不得用于商业用途。