返回介绍

10.4 Vector 类第2版:可切片的序列

发布于 2024-02-05 21:59:47 字数 5822 浏览 0 评论 0 收藏 0

如 FrenchDeck 类所示,如果能委托给对象中的序列属性(如 self._components 数组),支持序列协议特别简单。下述只有一行代码的 __len__ 和 __getitem__ 方法是个好的开始:

class Vector:
  # 省略了很多行
  # ...

  def __len__(self):
    return len(self._components)

  def __getitem__(self, index):
    return self._components[index]

添加这两个方法之后,就能执行下述操作了:

>>> v1 = Vector([3, 4, 5])
>>> len(v1)
3
>>> v1[0], v1[-1]
(3.0, 5.0)
>>> v7 = Vector(range(7))
>>> v7[1:4]
array('d', [1.0, 2.0, 3.0])

可以看到,现在连切片都支持了,不过尚不完美。如果 Vector 实例的切片也是 Vector 实例,而不是数组,那就更好了。前面那个 FrenchDeck 类也有类似的问题:切片得到的是列表。对 Vector 来说,如果切片生成普通的数组,将会缺失大量功能。

想想内置的序列类型,切片得到的都是各自类型的新实例,而不是其他类型。

为了把 Vector 实例的切片也变成 Vector 实例,我们不能简单地委托给数组切片。我们要分析传给 __getitem__ 方法的参数,做适当的处理。

下面来看 Python 如何把 my_seq[1:3] 句法变成传给 my_seq.__getitem__(...) 的参数。

10.4.1 切片原理

一例胜千言,我们来看看示例 10-4。

示例 10-4 了解 __getitem__ 和切片的行为

>>> class MySeq:
...   def __getitem__(self, index):
...     return index  # ➊
...
>>> s = MySeq()
>>> s[1]  # ➋
1
>>> s[1:4]  # ➌
slice(1, 4, None)
>>> s[1:4:2]  # ➍
slice(1, 4, 2)
>>> s[1:4:2, 9]  # ➎
(slice(1, 4, 2), 9)
>>> s[1:4:2, 7:9]  # ➏
(slice(1, 4, 2), slice(7, 9, None))

❶ 在这个示例中,__getitem__ 直接返回传给它的值。

❷ 单个索引,没什么新奇的。

❸ 1:4 表示法变成了 slice(1, 4, None)。

❹ slice(1, 4, 2) 的意思是从 1 开始,到 4 结束,步幅为 2。

❺ 神奇的事发生了:如果 [] 中有逗号,那么 __getitem__ 收到的是元组。

❻ 元组中甚至可以有多个切片对象。

现在,我们来仔细看看 slice 本身,如示例 10-5 所示。

示例 10-5 查看 slice 类的属性

>>> slice  # ➊
<class 'slice'>
>>> dir(slice)  # ➋
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
 '__format__', '__ge__', '__getattribute__', '__gt__',
 '__hash__', '__init__', '__le__', '__lt__', '__ne__',
 '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
 'indices', 'start', 'step', 'stop']

❶ slice 是内置的类型(2.4.2 节首次出现)。

❷ 通过审查 slice,发现它有 start、stop 和 step 数据属性,以及 indices 方法。

在示例 10-5 中,调用 dir(slice) 得到的结果中有个 indices 属性,这个方法有很大的作用,但是鲜为人知。help(slice.indices) 给出的信息如下。

S.indices(len) -> (start, stop, stride)

给定长度为 len 的序列,计算 S 表示的扩展切片的起始(start)和结尾(stop)索引,以及步幅(stride)。超出边界的索引会被截掉,这与常规切片的处理方式一样。

换句话说,indices 方法开放了内置序列实现的棘手逻辑,用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片。这个方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内。

下面举几个例子。假设有个长度为 5 的序列,例如 'ABCDE':

>>> slice(None, 10, 2).indices(5)  # ➊
(0, 5, 2)
>>> slice(-3, None, None).indices(5)  # ➋
(2, 5, 1)

❶ 'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]

❷ 'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]

 写作本书时,在线版 Python 库参考好像还没有 slice.indices 方法的文档。2Python Python/C API 参考手册中有类似的 C 语言函数的文档,PySlice_GetIndicesEx。研究切片对象时,我在 Python 控制台中执行了 dir() 和 help(),这才发现 slice.indices() 方法。这也表明交互式控制台是个有价值的工具,能发现新事物。

2现在已经有了,参见:https://docs.python.org/3/reference/datamodel.html?highlight=indices#slice.indices。——编者注

在 Vector 类中无需使用 slice.indices() 方法,因为收到切片参数时,我们会委托 _components 数组处理。但是,如果你没有底层序列类型作为依靠,那么使用这个方法能节省大量时间。

现在我们知道如何处理切片了,下面来看 Vector.__getitem__ 方法改进后的实现。

10.4.2 能处理切片的__getitem__方法

示例 10-6 列出了让 Vector 表现为序列所需的两个方法:__len__ 和 __getitem__ (后者现在能正确地处理切片了)。

示例 10-6 vector_v2.py 的部分代码:为 vector_v1.py 中的 Vector 类(见示例 10-2)添加 __len__ 和__getitem__ 方法

  def __len__(self):
    return len(self._components)

  def __getitem__(self, index):
    cls = type(self)  ➊
    if isinstance(index, slice):  ➋
      return cls(self._components[index])  ➌
    elif isinstance(index, numbers.Integral):  ➍
      return self._components[index]  ➎
    else:
      msg = '{cls.__name__} indices must be integers'
      raise TypeError(msg.format(cls=cls))  ➏

❶ 获取实例所属的类(即 Vector),供后面使用。

❷ 如果 index 参数的值是 slice 对象……

❸ ……调用类的构造方法,使用 _components 数组的切片构建一个新 Vector 实例。

❹ 如果 index 是 int 或其他整数类型……3

3必须在 vector_v2.py 的开头加上 import numbers。——编者注

❺ ……那就返回 _components 中相应的元素。

❻ 否则,抛出异常。

 大量使用 isinstance 可能表明面向对象设计得不好,不过在 __getitem__ 方法中使用它处理切片是合理的。注意,示例 10-6 中测试时用的是 numbers.Integral,这是一个抽象基类(Abstract Base Class,ABC)。在 isinstance 中使用抽象基类做测试能让 API 更灵活且更容易更新,原因参见第 11 章。可惜,Python 3.4 的标准库中没有 slice 的抽象基类。

为了确定在 __getitem__ 的 else 子句中会抛出哪个异常,我在交互式控制台中查看了 'ABC'[1, 2] 的结果。我发现,Python 抛出的是 TypeError;我还从错误消息中复制了表述方式,“indices must be integers”。为了创建符合 Python 风格的对象,我们要模仿 Python 内置的对象。

把示例 10-6 中的代码添加到 Vector 类中之后,切片行为就正确了,如示例 10-7 所示。

示例 10-7 测试示例 10-6 中改进的 Vector.__getitem__ 方法

  >>> v7 = Vector(range(7))
  >>> v7[-1] ➊
  6.0
  >>> v7[1:4] ➋
  Vector([1.0, 2.0, 3.0])
  >>> v7[-1:] ➌
  Vector([6.0])
  >>> v7[1,2] ➍
  Traceback (most recent call last):
    ...
  TypeError: Vector indices must be integers

❶ 单个整数索引只获取一个分量,值为浮点数。

❷ 切片索引创建一个新 Vector 实例。

❸ 长度为 1 的切片也创建一个 Vector 实例。

❹ Vector 不支持多维索引,因此索引元组或多个切片会抛出错误。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文