目录

字典设置默认值的几种方法

前言

在使用 Python 字典的时候,通过 d[k] 来获取键 k 的值,如果 k 不存在,Python 会抛出KeyError异常,这符合 Python 信奉的“快速失败”哲学。我们可以通过try/except语句块来捕获异常,但有时候我们更愿意设置一个默认值,当要查询的键不存在的时候,直接返回默认值。

有几种方式可以满足这个需求,本文将介绍这些方法,并比较它们的异同。

注意

本文中的命令行示例,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python:3.7.9

 

get 方法

获取默认值最常用的方法就是字典自带的 get()方法:

1
2
3
>>> help (dict.get)
get(self, key, default=None, /)
    Return the value for key if key is in the dictionary, else default.

get()的第二个参数default表示当 key 不存在时,返回的默认值。它默认为 None。

示例1

假设有一个记载最近三天各个城市最高气温的文件 weather.dat,文件每行是某个城市在某天的最高气温,没有排序,文件内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
London, 16.2
Sydney, 14.7
Shanghai, 23.2
Tokyo, 19.3
Shanghai, 21.3
Sydney, 15.2
London, 15.6
Shanghai, 22
Tokyo, 20.9
Tokyo, 19.8
Sydney, 18.1
London, 16.9

现在我们做数据处理,读入这些数据后,以城市作为关键字,把它最近三天的最高气温存在列表中作为值,为了之后做相关统计。

可以用get()方法的default参数来初始化空列表,程序实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import re

weather_statistic = {}
with open('weather.dat') as fp:
  for line in fp:
    matched = re.match(r'(\w+),\s*(\d+\.?\d*)\n', line)
    if matched:
      city = matched.group(1)
      temperature = float(matched.group(2))
      temperatures = weather_statistic.get(city, [])   # Use get method to initialize empty list as value if key doesn't exist
      temperatures.append(temperature)
      weather_statistic[city] = temperatures

print(weather_statistic)

执行结果:

1
{'London': [16.2, 15.6, 16.9], 'Sydney': [14.7, 15.2, 18.1], 'Shanghai': [23.2, 21.3, 22.0], 'Tokyo': [19.3, 20.9, 19.8]}

上述代码中,我们先用正则表达式模块re解析每行数据,得到城市和最高气温,然后把它们添加到字典 weather_statistic 中。添加过程中,先用get()方法从字典中查找关键字 city,如果是第一次添加,即 city 不存在,则返回一个空列表,如果 city 存在,则返回它对应的列表,然后把 temperature 加入到返回的列表中,最后把 city 和更新的列表加入到字典中。之所以最后需要手动将列表加入到字典中,是因为如果之前get()方法返回的是空列表,这个空列表还没有在字典中。

这个例子需要注意的是,对字典进行了两次查询,第一次是调用get()方法的时候,第二次是最后把 city 和对应列表加入到字典中时。关于字典的工作方式,可以参考本人另一篇文章:深入浅出地理解 Python 中可散列类型的原理和实现(二)

 

setdefault 方法

另一个设置默认值的方法是字典的setdefault()方法,严格来说,这个是设置默认值方法,而不是获取默认值方法。但因为它为字典某个键设置默认值之后,会返回该值,所以用该方法既可以设置默认值,也能返回设定的默认值。

setdefault()方法的工作方式是:

1
2
3
4
>>> help (dict.setdefault)
setdefault(self, key, default=None, /)
    Insert key with a value of default if key is not in the dictionary.
    Return the value for key if key is in the dictionary, else default.

如果 key 在字典中,那么直接返回它的值,就像 d[key] 一样;如果 key 不存在,把 key 和 default 值添加到字典中,并返回 default 值。

示例2

示例1 代码的get()方法用setdefault()方法替代:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import re

weather_statistic = {}
with open('students.dat') as fp:
  for line in fp:
    matched = re.match(r'(\w+),\s*(\d+\.?\d*)\n', line)
    if matched:
      city = matched.group(1)
      temperature = float(matched.group(2))
      weather_statistic.setdefault(city, []).append(temperature)	# use setdefault to initialize empty list as value if key doesn't exist

print(weather_statistic)

执行结果:

1
{'London': [16.2, 15.6, 16.9], 'Sydney': [14.7, 15.2, 18.1], 'Shanghai': [23.2, 21.3, 22.0], 'Tokyo': [19.3, 20.9, 19.8]}

这里,只用一行代码就实现了原来三行代码的功能,也就是说,下面两种写法的功能是一样的:

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

1
2
3
my_list = my_dict.get(key, [])
my_list.append(new_value)
my_dict[key] = my_list

两者的作用相同,但用setdefault()字典只需要进行一次查询,如果 key 不存在,直接插入 key 和空列表,而无需在最后再插入。而第二种方法,如同 示例1 解释,需要查询两次。

 

使用 defaultdict 类

Python 的 collections 模块提供了一个 defaultdict 类,它的作用和 dict 类似, 但如果查询的 key 不存在,它可以返回设定的默认值。

在创建一个 defaultdict 对象的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在__getitem__方法找不到 key 时被调用,得到一个默认对象,让__getitem__返回它。

比如,我们新建一个字典 d = defaultdict(list),如果 key 在 d 中不存在的话,表达式 d[key] 会按照以下步骤操作:

  1. 调用 list() 新建一个列表对象;
  2. 把这个列表对象作为值,key 作为键,放入 d 中;
  3. 返回这个列表对象的引用。

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

示例3

把先前统计城市最高气温的例子,用defaultdict来实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import re
import collections

weather_statistic = collections.defaultdict(list)
with open('students.dat') as fp:
  for line in fp:
    matched = re.match(r'(\w+),\s*(\d+\.?\d*)\n', line)
    if matched:
      city = matched.group(1)
      temperature = float(matched.group(2))
      weather_statistic[city].append(temperature)	# if key doesn't exist, it won't throw exception, otherwise it initializes an empty list as value


print(weather_statistic)
print(weather_statistic.default_factory)

执行结果:

1
2
defaultdict(<class 'list'>, {'London': [16.2, 15.6, 16.9], 'Sydney': [14.7, 15.2, 18.1], 'Shanghai': [23.2, 21.3, 22.0], 'Tokyo': [19.3, 20.9, 19.8]})
<class 'list'>

可以看到,用defaultdict类来为字典设定默认值的方式,和 示例2 非常相似,也只需要字典进行一次查询即可。区别有两点:

  1. defaultdict类需要在创建字典时就指定默认值,而setdefault()方法则在每次调用时指定默认值,从这个角度来看,setdefault()更灵活些。
  2. defaultdict类设定的默认值是一个可调用对象,任何可调用对象都可以起作用,比如类、函数、实现__call__()方法的类的对象、lambda 表达式等,当 key 不存在时,就用这些可调用对象创建默认对象。而setdefault()方法直接指定默认值。

 

__missing__ 方法

我们知道,当执行 d[key] 查询字典中的 key 时,Python 会调用__getitem__()方法,如果 key 不存在,__getitem__()方法会抛出KeyError异常。其实,在抛出异常之前,__getitem__()方法会先检查该类有没有定义__missing__()方法,如果有,就调用它,而不抛出异常。默认情况下,dict 类没有定义该方法,但是它知道有这么一个东西存在。也就是说,如果有一个类继承了 dict,然后该类实现了__missing__()方法,那么在__getitem__()方法找不到 key 的时候,就会自动调用它,而不是抛出 KeyError 异常。

__missing__()方法只会被__getitem__()方法调用,而字典的get()方法和运算符 in会调用的__contains__()方法,都是用 C 语言实现高效查询,并不会调用__getitem__()方法。所以__missing__()方法的实现不会影响字典的get()方法和运算符 in的行为。

上一节介绍的defaultdict类,其实就是实现了__missing__()方法,创建defaultdict对象的时候,设定的可调用对象被赋给defaultdict对象的default_factory属性,当 d[key] 查询的 key 不存在时,调用__missing__()方法,该方法内会再调用default_factory()创建默认的对象作为 key 对应的默认值,并返回。这意味着default_factory也只对__getitem__()方法有作用,而不会影响字典的get()方法和运算符 in的行为。

示例4

对于统计城市最高气温的例子,我们通过定义一个继承自 dict 的子类,并实现它的__missing__()方法,来达到和前面几个例子一样的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import re

class my_dict(dict):
  def __init__(self, default_factory):
    self.default_factory = default_factory

  def __missing__(self, key):
    self[key] = self.default_factory()
    return self[key]


weather_statistic = my_dict(list)
with open('students.dat') as fp:
  for line in fp:
    matched = re.match(r'(\w+),\s*(\d+\.?\d*)\n', line)
    if matched:
      city = matched.group(1)
      temperature = float(matched.group(2))
      weather_statistic[city].append(temperature)	# we can use my_dict as defaultdict because we implement __missing__

print(weather_statistic)

执行结果:

1
{'London': [16.2, 15.6, 16.9], 'Sydney': [14.7, 15.2, 18.1], 'Shanghai': [23.2, 21.3, 22.0], 'Tokyo': [19.3, 20.9, 19.8]}

这个例子中,我们自定义了一个类 my_dict 继承自 dict,并实现了__missing__()方法。当要查询的 key 不在 my_dict 对象中,就会触发调用__missing__()方法。在该方法中,我们先用构造函数内设置的 default_factory 构造一个默认对象,然后执行 “self[key] = self.default_factory()” 把 key 和 该对象插入到 my_dict 对象中,该语句会调用父类 dict 的__setitem__()方法,最后返回该对象。

可以看到,该例子中使用 my_dict 对象的方法,和 示例3 中使用defaultdict对象的方法,几乎一样。其实 my_dict 类的实现,就是defaultdict类实现的变体。有兴趣的可以参考 Python 中defaultdict的实现。

需要注意,想要自定义一个和字典有类似行为的类,继承自 dict 并不是最好的办法。推荐的方法是继承自UserDict类。具体细节,可参考本人另一篇文章 巧用 Python 的 UserDict 和 UserList

 

参考

https://docs.python.org/3/library/collections.html

https://blog.finxter.com/python-__missing__-magic-method/


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」