随便水一篇「雕虫小技」,想到哪算哪。读本文前假设已读过这篇文章

在 Python 中如何编写一个自定义的字典类?大家可能被告诉要使用collections.abc中的类作为基类而不是dictdict也不是任何时候都不能做基类——当你没有重载任何内建方法时可以直接继承dict

但实际场景千变万化,我们不能被几条规则限制了我们的思考,我们是基于什么来选择基类的呢?

我们需要什么样的鸭子

Python 的类型系统和多态基于鸭子类型,只要这个对象有我需要的所有特性我就能使用它,不管它类型为何。那么针对自定义字典,都是鸭子,我们需要什么样的鸭子呢?

  • collections.UserDict: 机器鸭,拥有所有鸭子的技能。
  • collections.abc.Mapping1: 一个神奇的鸭子外壳,得按要求穿到身上,任你是什么东西都立即拥有了鸭子的技能,和长相。
  • dict: 鸭子本鸭,所有基于此的动物都是鸭子的基因变异。

给我翻译翻译

为什么这么说?collections.UserDict是开箱即用,还方便小量修改,要改哪个行为,直接覆写就好了。但核心数据结构是写死的,可自定义空间不大。与之相对,collections.abc.Mapping给了你很大自由度,它没有自带的__init__方法,数据是存在自身还是存在远端都全凭你决定。而用dict,要写自定义逻辑就得小心,容易造出四不像。

除此之外,大部分使用起来都和普通字典并无两样,除了两个地方,其中一个是isinstance,虽然有条最佳实践是「检查它的行为而不是类型」推荐尽量不用isinstance,实在要用也要用isinstance(obj, collections.abc.Mapping),这对于上述三种派生的类都能返回正确的结果。

还有一个地方,使用场景不如isinstance那样广泛,就是json.dumps,我认为这里绝对需要改进,因为json.dumps的策略选择是基于isinstance(obj, dict)2!Python 居然没有一个让json.dumps读取的魔法方法,方便自定义类支持 JSON 序列化。导致json.dumps的这一特性,只对dict的派生类生效。

dict 重回视野

有的时候用户期待这个对象在所有地方都兼容普通 dict 的行为,比如一个附带格式属性的 JSON 解析器,用户期待解析结果能正常用 Python 标准库的json序列化。这时告诉用户用json.dumps(dict(obj))并不是一个选项。为这支持这万恶的json.dumps必须重新考虑基类的选择了。

dict做基类,容易发生覆写不完全的问题,而collections.abc.恰好可以补上这些缺口。只需要实现协议要求的抽象方法即可。但数据存储方面,必须保存一份干净数据dict本身,这样才能正确使用依赖dict的方法。

举例说明


class MyDict(collections.abc.MutableMapping, dict):
    def __init__(self, data):
        dict.__init__(self, data)
        # 执行一些解析逻辑,把结果保存到属性中
        self._data = self._parse(data)

    def __getitem__(self, key):
        # 注意这里我们没有从dict本身取数据,这是完全可以的
        return self._data[key]

    def __setitem__(self, key, value):
        # 但写数据时必须同时更新dict中的数据
        dict.__setitem__(self, key, value)
        # 更新其他属性
        self._update_data(key, value)

    # 省略了一些必要方法

原则是在所有写数据的地方调用一次dict自身的方法3,例子中用的是value,但也可以是经过清洗后的一份数据,这样json.dumps(obj)就会产生这份干净数据序列化后的结果。

所以 Best practice 说得再好,也有可能有例外,思考为什么这么做更重要。


  1. 取决于是否可变可选择collections.abc.MutableMapping,下同。
  2. https://docs.python.org/zh-cn/3/library/json.html#json.JSONDecoder
  3. 注意这里无法使用super(),必须显式指定基类通过self传递自身