重新思考自定义容器类的实现
随便水一篇「雕虫小技」,想到哪算哪。读本文前假设已读过这篇文章 在 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的派生类生效。 [^2]: 见 https://docs.python.org/zh-cn/3/library/json.html#json.JSONDecoder 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 说得再好,也有可能有例外,思考为什么这么做更重要。
以上内容来自博客订阅源,版权归原作者。关注作者后可以在 关注列表 第一时间查看作者文章。
登录后,加入这场讨论。
登录参与