随便水一篇「雕虫小技」,想到哪算哪。读本文前假设已读过这篇文章
在 Python 中如何编写一个自定义的字典类?大家可能被告诉要使用collections.abc中的类作为基类而不是dict。dict也不是任何时候都不能做基类——当你没有重载任何内建方法时可以直接继承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 说得再好,也有可能有例外,思考为什么这么做更重要。
Footnotes
取决于是否可变可选择
collections.abc.MutableMapping,下同。 ↩见 https://docs.python.org/zh-cn/3/library/json.html#json.JSONDecoder ↩
注意这里无法使用
super(),必须显式指定基类通过self传递自身 ↩
