深入剖析序列的特殊方法

协议和鸭子类型

开始之前,我先介绍协议和鸭子类型。在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值不可能是索引。

 

  • 15
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值