36、Python之面向对象:容器类协议与collections.abc

南宫理的日志录 2024-10-15 10:48:25
引言

在上一篇文章中,我们通过定义类继承自MutableSequence实现了自定义容器类的效果,使得我们自定义的类,可以进行[]索引、切片,可以进行for循环遍历,可以使用len()函数计算元素个数等,其行为表现得就像是一个内置类一样。

文章的最后,简单介绍了相关魔术方法所表达的容器对象需要统一遵循的协议。

其实,MutableSequence所在的collections.abc模块中定义了一系列的抽象基类。这些类的设计描述了各种容器(比如:列表、集合、字典等)上的编程接口,也就是定义某种特定容器类需要遵循的“协议”。

这些抽象类可以有两个作用:

1、可以将这些抽象类作为基类,用于自定义容器类,这些类要实例化对象,需要实现这些抽象方法(协议),以达成模仿内置容器类型的效果,比如上一篇文章中的Team类。

2、可以作为类型检查,比如判断某个对象是否类似于一个序列、字典等。

今天我们将展开来看下collections.abc中的容器相关的抽象类,梳理相关抽象类的继承关系,从而更好地理解容器类协议。

容器类继承层次图

首先看下collections.abc中容器类相关的抽象类的继承关系图,先对这些有个全局性的掌握。

需要说明的是,并没有把所有的容器抽象类都列举出来,只是把比较重要在图里呈现了出来。

从图中可以看到,最顶层有三个抽象基类:Sized、Container和Iterable,Collection继承自这三个抽象基类。

Set、Mapping、Sequence都继承自Collection,这三个是三种不可变的容器类型的抽象类。它们又各自有一个Mutable的子类,分别是对应的可变容器类。

容器类协议

首先是基础的单一协议的抽象类:

1、Container:

所有容器的基类,定义了抽象方法__contains__(),用于实现in运算符。

2、Iterable:

支持迭代协议的对象的基类,定义了抽象方法__iter__()。

3、Iterator:

迭代器对象的基类,定义了抽象方法next(),继承自Iterable,提供了一个不执行任何操作的默认__iter__()方法的实现。

4、Sized:

可以确定元素个数的容器的基类,定义了抽象方法__len__()。

5、Hashable:

可用作散列表键的对象的基类,定义了抽象方法__hash__()。

其次是复合的多协议的抽象类:

6、Sequence:

类似于序列的对象的基类,继承自Container、Iterable和Sized,添加了抽象方法__getitem__()和__len__()。同时提供了__contains__()、__iter__()、__reversed__()、index()和count()的默认实现,这些方法是使用__getitem__()和__len__()方法实现的。

7、MutableSequence:

可变序列的基类,继承自Sequence,添加了抽象方法__setitem__()和__delitem__()。同时提供了append()、reverse()、extend()、pop()、remove()和__iadd__()的默认实现。所以,我们在上一篇文章中,定义Team继承MutableSequence时,可以调用append()方法等。

8、Set:

类似于集合的对象的基类,继承自Containter、Iterable和Sized,定义了抽象方法__len__()、__iter__()和__contains__()。同时还提供了__le__()、__lt__()、__eq__()、__ne__()、__gt__()、__ge__()、__and__()、__or__()、__xor__()、__sub__()和isdisjoint()的默认实现。

9、MutableSet:

可变集合的基类,继承自Set,同时添加了抽象方法add()和discard()。同时还提供了clear()、pop()、remove()、__ior__()、__iand__()、__ixor__()和__isub__()的默认实现。

10、Mapping:

支持映射(字典)查找的对象的基类,继承自Container、Iterable和Sized,定义了抽象方法__getitem__()、__len__()和__iter__()。同时提供了__contains__()、keys()、items()、values()、get()、__eq__()和__ne__()的默认实现。

11、MutableMapping:

可变映射(字典)查找对象的基类,继承自Mapping,添加了抽象方法__setitem__()和__delitem__()。同时还添加了pop()、popitem()、clear()、update()和setdefault()的默认实现。

基于上面对这些容器类协议的说明可知,当我们需要自定义容器类或者自定义的类需要模仿内置容器类型的某些行为时,只需要定义类继承需要模仿的协议对应的抽象类。比如,如果只需要支持in操作符,则只需要继承自Container,并实现__contains__()方法即可。

需要说明的是,当我们后面的文章中介绍到“鸭子类型”和魔术方法时,会看到,不继承自这些抽象基类也是可以模仿内置容器类型的行为的,以后再行展开。

关于内置容器类型

最后,再简单看一下内置类型,你觉得已经对这些内置类型再熟悉不过,但也许只是“灯下黑”。

虽然我们已经能够很熟练地使用Python中的内置容器类型,比如:list、tuple、set、dict等。但是,我们通过IDE,比如PyCharm只能看到一个简要的说明,并没有具体的实现,如图所示:

从说明中,可以看到list是一个内置的MutableSequence。

但是,如果我们运行下面代码,会觉得有些奇怪:

执行结果:

list的基类只有object,并没有MutableSequence。可是,当使用isinstance()进行实例对象判断,以及使用issubclass()进行子类判断时,返回的却是True,似乎有些矛盾。

我们都知道,这些内置类实际上是由C语言实现的,并作为Python解释器的一部分进行编译和优化,一个C语言实现的list类型,是怎么跟Python中的MutableSequence抽象基类产生联系的呢?

如果仔细查看collections.abc的源代码,不难发现:

在MutableSequence类定义之后,调用了register()方法,该方法可用于动态注册一个类,表明这个类是该抽象基类的虚拟子类,即便这个类没有显式地继承该抽象基类。

所以,即便list类的基类只有object,当我们使用isinstance()和issubclass()时,会返回True。

关于抽象基类的register()方法,后面有机会的话,跟元类的概念放在一起进行介绍。

总结

本文梳理了collections.abc模块中的容器抽象类继承关系,展示了继承关系图。并对其中的比较关键的抽象基类所承载的容器类协议作了逐一说明。然后简单验证了内置容器类list与MutableSequence之间的关系。

感谢您的拨冗阅读,如果本文的内容对您学习Python有所帮助,欢迎点赞、收藏。

0 阅读:0

南宫理的日志录

简介:深耕IT科技,探索技术与人文的交集