NEP 27 — 零秩数组#

作者:

Alexander Belopolsky (sasha),由 Matt Picus 转录 <matti.picus@gmail.com>

状态:

最终版

类型:

信息性

创建:

2006-06-10

决议:

https://mail.python.org/pipermail/numpy-discussion/2018-October/078824.html

注意

NumPy 同时具有零秩数组和标量。这份设计文档改编自 2006 年的维基条目,描述了零秩数组是什么以及它们存在的原因。它于 2018 年 10 月 13 日被转录成 NEP,并且链接已更新。此拉取请求引发了关于 NumPy 中零秩数组和标量持续需求的 热烈讨论

这里的一些信息已过时,例如,现在已实现 0-D 数组的索引,并且不会出错。

零秩数组#

零秩数组是形状为 () 的数组。例如

>>> x = array(1)
>>> x.shape
()

零秩数组和数组标量#

数组标量在许多方面都类似于零秩数组

>>> int_(1).shape
()

它们甚至打印相同的结果

>>> print int_(1)
1
>>> print array(1)
1

但是,有一些重要的区别

  • 数组标量是不可变的

  • 对于不同的数据类型,数组标量具有不同的 Python 类型

数组标量的动机#

NumPy 的设计决策是在原生 Python 类型之外还提供 0 维数组和数组标量,这违背了 Python 的一个基本设计原则:只应该有一种显而易见的方法来做这件事。在本节中,我们将尝试解释为什么需要三种不同的方法来表示一个数字。

有几个 numpy-discussion 线程

曾多次建议 NumPy 只使用秩 0 数组来表示所有情况下的标量。将秩 0 数组转换为标量的优缺点总结如下:

  • 优点

    • 在某些情况下,Python 期望一个整数(最明显的是切片和索引序列时:ceval.c 中的 _PyEval_SliceIndex),它不会在引发错误之前先尝试将其转换为整数。因此,拥有由数组对象为您转换的 0 维数组很方便。

    • 没有因为拥有两种几乎相同但并不完全相同且其单独存在只能由 Python 和 NumPy 的开发历史解释的类型而导致用户混淆的风险。

    • 不会对执行显式类型检查的代码 (isinstance(x, float)type(x) == types.FloatType) 造成问题。尽管显式类型检查通常被认为是不好的做法,但有几个正当理由可以使用它们。

    • 不会在 pickle 文件中创建对 Numeric 的依赖关系(尽管这也可以通过 pickle 代码中数组的特殊情况来完成)

  • 缺点

    • 很难编写通用代码,因为标量没有与数组相同的 method 和属性。(例如 .type.shape)。Python 标量也具有不同的数值行为。

    • 这导致了令人不愉快的特殊情况检查。从根本上说,它让用户相信多维同质数组有点像 Python 列表(除了对象数组,它们不是)。

NumPy 实现了一种旨在拥有所有优点而没有上述任何缺点的解决方案。

为所有 21 种类型创建 Python 标量类型,并从已存在的三个类型继承。为这些 Python 标量类型定义等效的 method 和属性。

零秩数组的需求#

一旦拒绝了使用零秩数组来表示标量的想法,自然就会考虑是否可以完全消除零秩数组。但是,在一些重要的用例中,零秩数组无法被数组标量取代。另见 2006 年 2 月的 秩 0 数组的案例

  • 输出参数

    >>> y = int_(5)
    >>> add(5,5,x)
    array(10)
    >>> x
    array(10)
    >>> add(5,5,y)
    Traceback (most recent call last):
         File "<stdin>", line 1, in ?
    TypeError: return arrays must be of ArrayType
    
  • 共享数据

    >>> x = array([1,2])
    >>> y = x[1:2]
    >>> y.shape = ()
    >>> y
    array(2)
    >>> x[1] = 20
    >>> y
    array(20)
    

零秩数组的索引#

从 NumPy 0.9.3 版本开始,零秩数组不支持任何索引

>>> x[...]
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
IndexError: 0-d arrays can't be indexed.

另一方面,有几种情况对零秩数组有意义。

省略号和空元组#

Alexander 在 scipy-dev 上开始了 2006 年 1 月的讨论,提出了以下提案:

……允许 a[...] 可能是合理的。这样,省略号可以解释为任意数量的 :,包括零。另一个对标量有意义的下标运算符是 a[...,newaxis] 甚至 a[{newaxis, }* ..., {newaxis,}*],其中 {newaxis,}* 代表任意数量用逗号分隔的 newaxis 令牌。这将允许在适用于任何 NumPy 类型的通用代码中使用省略号。

Francesc Altet 支持在零秩数组上使用 [...] 的想法,并 建议 也支持 [()]

Francesc 的提案是:

In [65]: type(numpy.array(0)[...])
Out[65]: <type 'numpy.ndarray'>

In [66]: type(numpy.array(0)[()])   # Indexing a la numarray
Out[66]: <type 'int32_arrtype'>

In [67]: type(numpy.array(0).item())  # already works
Out[67]: <type 'int'>

人们一致认为,对于零秩数组 xx[...]x[()] 都应该有效,但问题仍然在于结果的类型是什么——零秩 ndarray 还是 x.dtype

(Alexander)

首先,无论对x[...]x[()]做出何种选择,它们都应该相同,因为...仅仅是“根据需要添加任意数量的:”的语法糖,在零秩的情况下,这意味着... = (:,)*0 = ()。其次,零秩数组和NumPy标量类型在NumPy内部是可互换的,但是NumPy标量可以在某些Python结构中使用,而ndarray却不行。例如:

>>> (1,)[array(0)]
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: tuple indices must be integers
>>> (1,)[int32(0)]
1

由于大多数(如果不是全部的话)NumPy函数在返回时会自动将零秩数组转换为标量,因此[...][()]操作没有理由不同。

参见SVN变更集1864(已成为git提交9024ff0),了解x[...]x[()]返回NumPy标量的实现。

参见SVN变更集1866(已成为git提交743d922),了解x[...] = vx[()] = v的实现。

使用newaxis提升秩#

所有评论者都喜欢此功能,因此从SVN变更集1871(已成为git提交b32744e)开始,可以在零秩数组的下标参数中放置任意数量的省略号和newaxis标记。例如:

>>> x = array(1)
>>> x[newaxis,...,newaxis,...]
array([[1]])

目前尚不清楚为什么应允许使用多个省略号,但这正是我们试图保留的高秩数组的行为。

重构#

目前,所有对零秩数组的索引操作都在if (nd == 0)代码分支中实现,该分支以前总是引发索引错误。这确保了更改不会影响任何现有用法(除了依赖异常的用法)。另一方面,这些更改的部分动机是使ndarray的行为更加统一,这应该允许完全消除if (nd == 0)检查。