协议和鸭子类型
开始之前,我先介绍协议和鸭子类型。在Python中创建功能完善的序列类型无需继承,实现符合序列协议的方法即可。那么说的协议是什么呢?
在面向对象编程中,协议是非正式的接口,只在文档中定义,不在代码中定义。例如:Python的序列协议只要实现__len__和__getitem__这两个特殊方法。任何类,只要使用标准的签名和语义实现了这两个方法,就能用在任何预期序列的地方。下面实现了一个简单的序列:
class MySequence:
def __init__(self, data):
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, index):
return self._data[index]
上面代码中实现了序列协议,即便代码中没有声明这一点,但是任何有经验的Python开发者只需看一眼就知道它是序列。即便它是object的子类也无妨,我们说它是序列,因为它的行为是序列,这才是重点。
协议是非正式的,没有强制力,因此如果知道类的具体使用场景,那么通常只需要实现协议的一部分。例如,为了支持迭代,只需要实现__getitem__方法,没必要提供__len__方法。
可切片的序列
如上述的MySequence类所示,添加了__len__和__getitem__这两个方法以后,以下的切片操作都能执行了:
s = MySequence([1, 2, 3, 4, 5])
print(len(s)) # 5
print(s[0]) # 1
print(s[-1]) # 5
s1 = MySequence(range(11, 20))
print(list(s1[2:5])) # [13, 14, 15]
s2 = MySequence((1, 2, 3, 4, 5))
print(s2[1:-2]) # (2,3)
可以看出,连切片都支持了,不过还尙不完美。为什么呢,我们想想内置的序列类型:切片得到的都是各自类型的新实例,而不是其他类型。为了把MySequence实例的切片也变成MySequence实例,不能简单的把切片操作委托给列表,要分析传给__getitem__方法的参数,做适当的处理。
切片原理
一例胜千言:
class MySequence:
def __getitem__(self, index):
return index
s = MySequence()
# 单个索引,输出1
print(s[1])
# 1:4变成了slice(1, 4, None)
print(s[1:4])
# 输出slice(1, 4, 2),意思是从1到4,步幅为2
print(s[1:4:2])
# 输出(slice(1, 4, 2), 9),如果[]里面有逗号,那么__getitem__得到的是元组
print(s[1:4:2,9])
# 输出(slice(1, 4, 2), slice(7, 9, None))
print(s[1:4:2, 7:9])
现在我们来看看slice本身,slice是内置的类型,查看slice,我们发现它有start,stop,step这三种数据属性,还有一个indices方法。
>>> dir(slice)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '
__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']
indices这是一个方法,作用很大,但是鲜为人知。help(slice.indices)给出的信息如下:
S.indices(len) -> (start, stop, stride)。给定长度为len的序列,计算S表示的扩展切片的起始(start)索引和结尾(stop)索引,以及步幅(stride)。超出边界的索引会被裁掉。
举个例子:
print(slice(None, 10, 2).indices(5)) # 输出(0,5,2),'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]
print(slice(-3, None, None).indices(5)) # 输出(2,5,1),'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]
我们也可以把上面的MySequence使用slice进行改写:
class MySequence:
def __init__(self, data):
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, slice_obj: slice):
start, stop, step = slice_obj.indices(len(self._data))
return [self._data[i] for i in range(start, stop, step)]
这是切片的原理。
接下来我们就开始接上面,把MySequence实例的切片也变成MySequence实例。
import operator
class MySequence:
def __init__(self, data):
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, key):
if isinstance(key, slice):
cls = type(self)
return cls(self._data[key])
index = operator.index(key)
return self._data[index]
s = MySequence(range(7))
print(s[-1])
print(s[1:3])
上面代码意思就是如果key是slice对象,就获取一个实例的类,然后调用类的构造函数,使用_data数组的切片构造一个新的Mysequence实例。如果是单个索引则返回对于的元素。
值得一提的是这里使用的operator.index(),这个函数背后调用的特殊方法是__index__。目的是支持Numpy中众多整数类型作为索引和切片参数。operator.index()和int()之间的主要区别是,前者只有一个用途。例如int(3.14)返回3,而operator.index()返回TypeError,因为float值不可能是索引。