Python中自定义序列类

这篇文章我们会了解到Python的序列协议是什么,以及通过序列协议使类或者实例转变为序列。

Python中的内置序列分类

我们按照两个维度来区分序列

第一个维度是按照序列存贮的数据类型,分为容器序列和扁平序列

1
2
容器序列
list, tuple, deque
1
2
扁平序列
str, bytes, bytearray, array.array

容器序列里面可以放任意类型的数据。扁平序列的数据类型一致。

注意:array和list的一个重要区别, array只能存放指定的数据类型

第二个维度是按照序列是否可变,分为可变序列和不可变序列

1
2
可变序列
list, deque, bytearray, array
1
2
不可变
str, tuple, bytes

Python中序列类型的abc继承关系

这一小节我们将通过abc继承关系来讲下序列协议

跟容器相关的数据结构的抽象基类都存在_collections_abc.py模块下。

1
"Sequence"(不可变序列), "MutableSequence"(可变序列),

我们看下不可变序列(Sequence)的源码是由哪些抽象函数协议组成的。

1
2
3
4
5
6
7
class Sequence(Reversible, Collection):

"""All the operations on a read-only sequence.

Concrete subclasses must override __new__ or __init__,
__getitem__, and __len__.
"""

Sequence继承了Reversible(用于翻转)和Collection。

我们再看看Collection的源码。

1
2
3
4
5
6
7
8
9
class Collection(Sized, Iterable, Container):

__slots__ = ()

@classmethod
def __subclasshook__(cls, C):
if cls is Collection:
return _check_methods(C, "__len__", "__iter__", "__contains__")
return NotImplemented

Collection 又分别继承Sized, Iterable, Container。我们看下这三个类的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Sized(metaclass=ABCMeta):

__slots__ = ()

@abstractmethod
def __len__(self):
return 0

@classmethod
def __subclasshook__(cls, C):
if cls is Sized:
return _check_methods(C, "__len__")
return NotImplemented

Sized实现了__len__使我们的序列具有长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Iterable(metaclass=ABCMeta):

__slots__ = ()

@abstractmethod
def __iter__(self):
while False:
yield None

@classmethod
def __subclasshook__(cls, C):
if cls is Iterable:
return _check_methods(C, "__iter__")
return NotImplemented

Iterable实现了__iter__使我们的序列可以迭代(for 操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Container(metaclass=ABCMeta):

__slots__ = ()

@abstractmethod
def __contains__(self, x):
return False

@classmethod
def __subclasshook__(cls, C):
if cls is Container:
return _check_methods(C, "__contains__")
return NotImplemented

Container实现了__contains__使我们可以使用 is in 判断是否存在序列中。

通过上述的魔法函数组成了构成不可变序列的协议。

对于可变序列MutableSequence,作为不可变序列Sequence的子类,我们看看它的源码多实现了哪些魔法函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MutableSequence(Sequence):

__slots__ = ()

"""All the operations on a read-write sequence.

Concrete subclasses must provide __new__ or __init__,
__getitem__, __setitem__, __delitem__, __len__, and insert().

"""

@abstractmethod
def __setitem__(self, index, value):
raise IndexError

@abstractmethod
def __delitem__(self, index):
raise IndexError

我们看到最主要的是新增了__setitem__用于赋值,__delitem__用于删除值。这两个魔法函数。

如果我们想自定义一些序列类,只需要实现上述魔法函数(协议)即可。

序列的+、+=和extend的区别

我们看下下面代码

1
2
3
4
5
6
a = [1, 2]
c = a + [3, 4]

# 就地加
a += (3, 4)
a += [3, 4]

对于 + 两边的数据类型必须一致,而 += 只需要是序列类型即可。

为什么 +=只要是序列就可以呢?

我们看看+=的实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MutableSequence(Sequence):

__slots__ = ()

"""All the operations on a read-write sequence.

Concrete subclasses must provide __new__ or __init__,
__getitem__, __setitem__, __delitem__, __len__, and insert().

"""
def extend(self, values):
'S.extend(iterable) -- extend sequence by appending elements from the iterable'
for v in values:
self.append(v)

def __iadd__(self, values):
self.extend(values)
return self

在可变序列MutableSequence中的__iadd__就是实现 +=操作的,我们看到中间有调用

extend,我们看看extend函数有要求的是可迭代类型。

对于extend:

1
2
3
4
5
6

a.extend(range(3))

def extend(self, iterable): # real signature unknown; restored from __doc__
""" L.extend(iterable) -> None -- extend list by appending elements from the iterable """
pass

我们看到extend内置源码实现原理接收一个可迭代对象。

实现可切片的对象

下面是Python序列切片使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 模式[start:end:step]
"""
其中,第一个数字start表示切片开始位置,默认为0;
第二个数字end表示切片截止(但不包含)位置(默认为列表长度);
第三个数字step表示切片的步长(默认为1)。
当start为0时可以省略,当end为列表长度时可以省略,
当step为1时可以省略,并且省略步长时可以同时省略最后一个冒号。
另外,当step为负整数时,表示反向切片,这时start应该比end的值要大才行。
"""
aList = [3, 4, 5, 6, 7, 9, 11, 13, 15, 17]
aList[::] # 返回包含原列表中所有元素的新列表
aList[::-1] # 返回包含原列表中所有元素的逆序列表
aList[::2] # 隔一个取一个,获取偶数位置的元素
aList[1::2] # 隔一个取一个,获取奇数位置的元素
aList[3:6] # 指定切片的开始和结束位置
aList[0:100] # 切片结束位置大于列表长度时,从列表尾部截断
aList[100:] # 切片开始位置大于列表长度时,返回空列表

aList[len(aList):] = [9] # 在列表尾部增加元素
aList[:0] = [1, 2] # 在列表头部插入元素
aList[3:3] = [4] # 在列表中间位置插入元素
aList[:3] = [1, 2] # 替换列表元素,等号两边的列表长度相等
aList[3:] = [4, 5, 6] # 等号两边的列表长度也可以不相等
aList[::2] = [0] * 3 # 隔一个修改一个
aList[::2] = ['a', 'b', 'c'] # 隔一个修改一个
aList[::2] = [1, 2] # 左侧切片不连续,等号两边列表长度必须相等
aList[:3] = [] # 删除列表中前3个元素

del aList[:3] # 切片元素连续
del aList[::2] # 切片元素不连续,隔一个删一个

下面是自定义一个不可变得序列类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import numbers


class Group:
#支持切片操作
# staffs 是一个list以便于实现对数据的管理
def __init__(self, group_name, company_name, staffs):
self.group_name = group_name
self.company_name = company_name
self.staffs = staffs

def __reversed__(self):
# 用于对数据的反转
self.staffs.reverse()

def __getitem__(self, item):
# 切片主要的实现函数
# item有两类
# 当我们使用[:2]这种方式的时候item是切片类 item = {slice} slice(None, 2, None)
# 当使用[2]这种方式的时候item是一个int类型 item = {int} 2
# 下面返回的仍然是一个Group方便切片之后仍然可以切片
cls = type(self)
if isinstance(item, slice):
return cls(group_name=self.group_name, company_name=self.company_name, staffs=self.staffs[item])
elif isinstance(item, numbers.Integral):
return cls(group_name=self.group_name, company_name=self.company_name, staffs=[self.staffs[item]])

def __len__(self):
# 返回长度
return len(self.staffs)

def __iter__(self):
# 可迭代
return iter(self.staffs)

def __contains__(self, item):
# 使用 is in
if item in self.staffs:
return True
else:
return False


staffs = ["staff1", "staff2", "staff3", "staff4"]
group = Group(company_name="alibaba", group_name="user", staffs=staffs)

bisect维护已排序序列

注意:一定要是一个已排序的序列

我们先看下这个模块的源码结构

bisect代码结构

其中的insort为插入数据是insort_right的缩写,bisect为查询插入位置是bisect_right的缩写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
In [11]: import bisect

In [12]: inter_list = []

# 我们使用insort插入数据 排序查找规则是二分法
In [13]: bisect.insort(inter_list, 3)

In [14]: bisect.insort(inter_list, 4)

In [15]: bisect.insort(inter_list, 1)

In [16]: bisect.insort(inter_list, 2)

In [17]: bisect.insort(inter_list, 5)

In [18]: inter_list
Out[18]: [1, 2, 3, 4, 5]

# 我们使用bisect查找应该插入的位置
# bisect_right表示在查找到重复值的右侧插入
In [19]: bisect.bisect(inter_list, 3)
Out[19]: 3

# bisect_left表示在查找到重复值的左侧插入
In [20]: bisect.bisect_left(inter_list, 3)
Out[20]: 2

# 对应的插入
In [21]: bisect.insort(inter_list, 3)

In [22]: inter_list
Out[22]: [1, 2, 3, 3, 4, 5]

In [23]: bisect.insort_left(inter_list, 3)

In [24]: inter_list
Out[24]: [1, 2, 3, 3, 3, 4, 5]

看一篇bisect的妙用文章:Python-bisect模块的妙用

什么时候不该使用列表

有时对于使用list对性能有很大影响或者,我们可以考虑使用其他序列如array, deque。

1
2
3
4
5
6
import array

# array和list的一个重要区别, array只能存放指定的数据类型
# i 表示整型
my_array = array.array("i")
my_array.append(1)

array支持的数据类型查看 array

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!