第18条 学会利用__missing__构造依赖键的默认值
内置的dict
类型提供了setdefault
方法,在特殊场合可以用这个方法处理缺失的键,这样做要比其他方案少写一些代码(参见第16条)。然而,对于一般的情况来说,还是应该考虑内置的collections
模块中的defaultdict
类型(参见第17条)。当然,也有一些任务是setdefault
和defaultdict
都处理不好的。
例如,我们要写一个程序,在文件系统里管理社交网络账号中的图片。这个程序应该用字典把这些图片的路径名跟相关的文件句柄(file handle)关联起来,这样我们就能方便地读取并写入图像了。下面,先用普通的dict
实例实现。我们把get
方法与赋值表达式(Python 3.8版引入,参见第10条)结合起来,判断字典里有没有当前要操作的这个键。
如果字典里已经有这个handle
了,那么这种写法只需要进行一次字典访问。如果没有,那么它会通过get
方法访问一次字典,然后在try/except/else
结构的else
分支中做一次赋值(带finally
块的结构也可以这样做,参见第65条)。读取数据的代码与打开文件并处理异常的代码可以分开写。
这套逻辑也能用in
表达式或KeyError
实现,但那两种方案的字典访问次数与代码嵌套层数都比较多。有人可能觉得,既然这套逻辑能用get
、in
与KeyError
这三种方案实现,那么也应该可以用第四种方案,也就是setdefault
方法来实现。
这样写有很多问题。首先,即便图片的路径名已经在字典里了,程序也还是得调用内置的open
函数创建文件句柄,于是导致这个程序要给已经创建过handle的那份文件再度创建handle(两者可能相互冲突)。另外,如果try
块抛出异常,那我们可能无法判断这个异常是open
函数导致的,还是setdefault
方法导致的,因为这两次调用全都写在了同一行代码里(其他一些类似字典的实现方案或许可以做到,参见第43条)。
如果要把这套逻辑用作内部状态的管理,那么可能还会想到第五种方案,就是用defaultdict
来记录跟踪这些图片。下面用defaultdict
类实现相同的逻辑,只不过这次得专门写一个辅助函数。
程序出错的原因在于,传给defaultdict
的那个函数只能是不需要参数的函数,而我们写的辅助函数却要求调用方传入一个参数。defaultdict
并不知道当前要访问的这个键叫什么名字,所以没办法给辅助函数传递这个参数,这也意味着我们没办法用这个参数去调用open
。这种情况下,setdefault
与defaultdict
都无法满足需求。
这种需求很常见,因此Python内置了一种解决方案,可以通过继承dict
类型并实现__missing__
特殊方法来解决这个问题。我们可以把字典里不存在这个键时所要执行的逻辑写在这个方法中。下面定义一个新的类,让它利用刚才写的那个辅助函数来实现__missing__
方法。
访问pictures[path]
时,如果pictures
字典里没有path
这个键,那就会调用__missing__
方法。这个方法必须根据key
参数创建一份新的默认值,系统会把这个默认值插入字典并返回给调用方。以后再访问pictures[path]
,就不会调用__missing__
了,因为字典里已经有了对应的键与值(类似的机制还体现在__getattr__
中,参见第47条)。
要点
- 如果创建默认值需要较大的开销,或者可能抛出异常,那就不适合用
dict
类型的setdefault
方法实现。 - 传给
defaultdict
的函数必须是不需要参数的函数,所以无法创建出需要依赖键名的默认值。 - 如果要构造的默认值必须根据键名来确定,那么可以定义自己的
dict
子类并实现__missing__
方法。