就创造自定义映射类型来说,以 UserDict 为基类,总比以普通的 dict 为基类要来得方便。

这体现在,我们能够改进示例 3-7 中定义的 StrKeyDict0 类,使得所有的键都存储为字符串类型。

而更倾向于从 UserDict 而不是从 dict 继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是 UserDict 就不会带来这些问题。5

5关于从 dict 或者其他内置类继承到底有什么不好,详见 12.1 节。

另外一个值得注意的地方是,UserDict 并不是 dict 的子类,但是 UserDict 有一个叫作 data 的属性,是 dict 的实例,这个属性实际上是 UserDict 最终存储数据的地方。这样做的好处是,比起示例 3-7,UserDict 的子类就能在实现 __setitem__ 的时候避免不必要的递归,也可以让 __contains__ 里的代码更简洁。

多亏了 UserDict,示例 3-8 里的 StrKeyDict 的代码比示例 3-7 里的 StrKeyDict0 要短一些,功能却更完善:它不但把所有的键都以字符串的形式存储,还能处理一些创建或者更新实例时包含非字符串类型的键这类意外情况。

示例 3-8 无论是添加、更新还是查询操作,StrKeyDict 都会把非字符串的键转换为字符串

import collections

class StrKeyDict(collections.UserDict):  ➊

    def __missing__(self, key):  ➋
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]

    def __contains__(self, key):
        return str(key) in self.data  ➌

    def __setitem__(self, key, item):
        self.data[str(key)] = item  ➍

StrKeyDict 是对 UserDict 的扩展。

__missing__ 跟示例 3-7 里的一模一样。

__contains__ 则更简洁些。这里可以放心假设所有已经存储的键都是字符串。因此,只要在 self.data 上查询就好了,并不需要像 StrKeyDict0 那样去麻烦 self.keys()

__setitem__ 会把所有的键都转换成字符串。由于把具体的实现委托给了 self.data 属性,这个方法写起来也不难。

因为 UserDict 继承的是 MutableMapping,所以 StrKeyDict 里剩下的那些映射类型的方法都是从 UserDictMutableMappingMapping 这些超类继承而来的。特别是最后的 Mapping 类,它虽然是一个抽象基类(ABC),但它却提供了好几个实用的方法。以下两个方法值得关注。

MutableMapping.update

  这个方法不但可以为我们所直接利用,它还用在 __init__ 里,让构造方法可以利用传入的各种参数(其他映射类型、元素是 (key, value) 对的可迭代对象和键值参数)来新建实例。因为这个方法在背后是用 self[key] = value 来添加新值的,所以它其实是在使用我们的 __setitem__ 方法。

Mapping.get

  在 StrKeyDict0(示例 3-7)中,我们不得不改写 get 方法,好让它的表现跟 __getitem__ 一致。而在示例 3-8 中就没这个必要了,因为它继承了 Mapping.get 方法,而 Python 的源码(https://hg.python.org/cpython/file/3.4/Lib/_collections_abc.py#l422)显示,这个方法的实现方式跟 StrKeyDict0.get 是一模一样的。

 在写完 StrKeyDict 这个类之后,我读到了 Antonie Pitrou 写的“PEP 455 — Adding a key-transforming dictionary to collections”(https://www.python.org/dev/peps/pep-0455/)。文章附带的补丁里包含了一个叫作 TransformDict 的新类型。这个补丁通过 issue 18986(http://bugs.python.org/issue18986)被吸收进了 Python 3.5。为了试试这个类,我把它提取出来放进了一个单独的模块(在本书代码仓库中:03-dict-set/transformdict.py,https://github.com/fluentpython/example-code/blob/master/03-dict-set/transformdict.py)。比起 StrKeyDictTransformDict 的通用性更强,也更复杂,因为它把键存成字符串的同时,还要按照它原来的样子存一份。

之前我们见识过了不可变的序列类型,那有没有不可变的字典类型呢?这么说吧,在标准库里是没有这样的类型的,但是可以用替身来代替。