30.2.8. 利用__missing__构造依赖键的默认值

内置的dict类型提供了setdefault方法,在特殊场合可以用这个方法处理缺失的键,这样做要比其他方案少写一些代码。

然而,对于一般的情况来说,还是应该考虑内置的collections模块中的defaultdict类型。

当然,也有一些任务是setdefault和defaultdict都处理不好的。

例如,我们要写一个程序,在文件系统里管理社交网络账号中的图片。这个程序应该用字典把这些图片的路径名跟相关的文件句柄关联起来,这样我们就能方便地读取并写入图像了。 下面先用普通的dict实例实现。

pictures = {}
path = 'profile_1234.png'

with open(path, 'wb') as f:
    f.write(b'image data here 1234')

if (handle := pictures.get(path)) is None:
    try:
        handle = open(path, 'a+b')
    except OSError:
        print(f'Failed to open path {path}')
        raise
    else:
        pictures[path] = handle

handle.seek(0)
image_data = handle.read()

print(pictures)
print(image_data)

这套逻辑也能用in表达式或KeyError实现,但那两种方案的字典访问次数与代码嵌套层数都比较多。有人可能认为,既然这套逻辑能用getinKeyError这三种方案实现,那么也应该可以用setdefault方法来实现。

try:
    handle = pictures.setdefault(path, open(path, 'a+b'))
except OSError:
    print(f'Failed to open path {path}')
    raise
else:
    handle.seek(0)
    image_data = handle.read()

用defaultdict类实现相同的逻辑,只不过这次得专门写一个辅助函数。

def open_pictrue(profile_path):
    try:
        return open(profile_path, "a+b")
    except OSError:
        print(f"Failed to open path {profile_path}")
    raise

此时,还有一种解决方案,通过继承dict类型并实现__missing__特殊方法来解决这个问题。我们可以把字典里不存在这个键时所要执行的逻辑写在这个方法中。

#!/usr/bin/env python
# -*- coding:utf8 -*-
# auther: 18793
# Date:2021/10/26 17:09
# filename: sample.py
from collections import defaultdict
from typing import Dict, Any

path = 'profile_1234.png'

with open(path, 'wb') as f:
    f.write(b'image data here 9239')


def open_pictrue(profile_path):
    try:
        return open(profile_path, "a+b")
    except OSError:
        print(f"Failed to open path {profile_path}")
    raise


class Pictures(dict):
    def __missing__(self, key):
        value = open_pictrue(key)
        self[key] = value
        return value


pictures = Pictures()
handle = pictures[path]
handle.seek(0)
image_data = handle.read()
print(pictures)
print(image_data)

访问pictures[path]时,如果pictures字典里没有path这个键,那就调用__missing__方法。这个方法必须根据key参数创建一份新的默认值,系统会把这个默认值插入字典并返回给调用放。 以后再访问pictures[path],就不会调用__missing__了,因为字典里已经有了对应的键与值。

要点:

如果创建默认值需要较大的开销,或者可能抛出异常,那就不适合用dict类型的setdefault方法实现。

传给defaultdict的函数必须是不需要参数的函数,所以无法创建出需要依赖键名的默认值。

如果要构造的默认值必须根据键名来确定,那么可以定义自己的dict子类并实现__missing__方法。