随便水一篇「雕虫小技」,想到哪算哪。读本文前假设已读过这篇文章
在 Python 中如何编写一个自定义的字典类?大家可能被告诉要使用collections.abc
中的类作为基类而不是dict
。dict
也不是任何时候都不能做基类——当你没有重载任何内建方法时可以直接继承dict
。
但实际场景千变万化,我们不能被几条规则限制了我们的思考,我们是基于什么来选择基类的呢?
我们需要什么样的鸭子
Python 的类型系统和多态基于鸭子类型,只要这个对象有我需要的所有特性我就能使用它,不管它类型为何。那么针对自定义字典,都是鸭子,我们需要什么样的鸭子呢?
collections.UserDict
: 机器鸭,拥有所有鸭子的技能。collections.abc.Mapping
1: 一个神奇的鸭子外壳,得按要求穿到身上,任你是什么东西都立即拥有了鸭子的技能,和长相。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
传递自身 ↩