Numpy: ndarray应该源自collections.abc.Sequence吗?

创建于 2012-12-01  ·  49评论  ·  资料来源: numpy/numpy

@juliantaylor在熊猫问题中提出了这个问题。

票证中的示例:

import numpy as np
import random
random.sample(np.array([1,2,3]),1)

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/user1/py33/lib/python3.3/random.py", line 298, in sample
    raise TypeError("Population must be a sequence or set.  For dicts, use list(d).")
TypeError: Population must be a sequence or set.  For dicts, use list(d).

在1.7.0rc1.dev-3a52aa0的3.3上以及在1.6.2的3.2上都会发生这种情况。
2.7当然不受影响。

来自cpython / Lib / random.py:297的相关代码

from collections.abc import Set as _Set, Sequence as _Sequence
def sample(self, population, k):
# ...
    if not isinstance(population, _Sequence):
        raise TypeError("Population must be a sequence or set.  For dicts, use list(d).")

我无法通过类似的测试在stdlib中grep另一个位置,但是lib2to3
确实显示了假定的等效性:

lib2to3/fixes/fix_operator.py
5:operator.isSequenceType(obj)   -> isinstance(obj, collections.Sequence)

在2.7中

In [6]: operator.isSequenceType(np.array([1]))
Out[6]: True

但在3.3 / 3.2中

>>> isinstance(np.array([1]), collections.Sequence)
False

最有用的评论

是的,只需在某处添加Sequence.register(np.ndarray)

所有49条评论

Python 3.x只是在random.sample比Python 2.x更严格的检查。 在2.x上,numpy也不是Sequence子类(ndarray没有indexcount__reversed__方法)。 因此,我认为您可以将其视为random.sample误用或Python 3.x的向后兼容性中断。

足够公平,但是也就是说,我们可以轻松地_array_ ndarray一个Sequence,
因为所有这些方法都是有意义的,并且对
实行。 实际上,从Sequence继承就足够了,因为Sequence
提供所有缺少的方法作为mixin。
2012年12月2日13:30,“ Ralf Gommers” [email protected]写道:

Python 3.x只是在random.sample中执行比
Python2.x。 在2.x上numpy也不是Sequence子类(ndarray没有
索引,计数或反向方法)。 所以我认为您可以将其视为
错误使用random.sample或向后兼容
Python3.x。

-
直接回复此电子邮件或在Gi tHub上查看它

如果不是2to3示例的话,我会说同样的话,这会使它变得更加微妙。
我认为不需要索引和计数:链接
Sequence所有抽象方法已经实现,其他方法具有默认实现。

问题是MutableSequence实际上更正确,而insert没有实现。

但是不要忘记0维数组,我不确定它们现在会发生什么,但是它们并不是一个真正的序列吗?

嗯, Sequence是为不可变的对象准备的, MutableSequence某些方法(扩展,弹出)没有意义。 所以是的,我们可以添加这三种方法,但感觉不太正确。

在我看来, random.sample确实没有理由要求Sequence实例。

__setitem__已实现,但未实现__delitem__ ,因此可能是SomewhatMutableSequence

您将接口解释为“序列应具有所有这些,但有些可能
具有糟糕的O()”,这就是为什么它们提供某些方法的幼稚实现的原因。
当然,在ndarray上下文中不清楚的不是insertpop的语义。
但是,我不确定在0维数组上进行迭代的正确方法是什么。
有人可能会争辩说提供__iter__方法只会提高TypeError而不是
无论如何, StopIteration违反了鸭子的打字。

编辑:虽然我确定这不是一个粗心的决定。

ndarray可以只是一个Sequence,并不意味着它必须是不可变的。

顺便说一句。 CPython列表实现也不支持有效的插入,弹出和扩展,但仍然是MutableSequence

ndarray无法支持就地插入/弹出/扩展(ndarray具有固定大小),因此,虽然它是可变序列,但它根本不是Python MutableSequence (并且永远不会)。 它可以并且应该支持Sequence接口。

如果random.sample不检查这一点会很好,但是仔细观察,它确实有一个合理的原因-对于不同类型的输入参数,它具有许多不同的实现,因此需要某种方式区分它们。 它不能只是开始索引并希望获得最好的结果。 也许我们可以提交一个错误并尝试说服他们默认情况下无法识别类型的序列实现,但是最早可以帮助的是3.4 ...

不过,关于0维数组的观点很不错。 0维数组不支持Sequence接口(甚至不支持Iterable )。 但是出于Python的目的,如果它们只是撒谎成为Sequence s然后等待实际访问引发错误,那就不是太糟糕了-鸭子输入意味着如果想要:-)总是会失败。 也许可以使多维数组isinstance(a, Sequence)成功而0-d数组失败。 如果我们能够做到这一点,那就太酷了。 但是,即使我们不能做的最好的事情,仍然可能是使ndarray成为Sequence s。

注意MaskedArray已经有一个count方法,因此在ndarray中添加一个执行不同操作的方法会破坏该方法。

最好使用一些方便的方法将0-D数组视为标量(至少我是这样认为的)。 除了不可迭代之外,它们也不可索引,如果将它们视为数组,这将变得更加奇怪。 因此,以另一种方式使0-D数组不一致并不是一件大问题。

@njsmith您在哪里看到多个实现? 经过isinstance(Sequence)检查之后,我只看到len(population) ,然后看到一个转换为列表。 http://hg.python.org/cpython/file/22d891a2d533/Lib/random.py

Pandas Series和DataFrame类型还具有不兼容的计数方法和索引属性。

@rgommers :嗯,是的,我被错误消息误导了,并认为它也接受整数作为range()的简写,但事实并非如此。 即使这样,他们也确实希望为集合,序列和映射定义不同的行为。 也许我们可以说服他们,他们应该将其切换到

if isinstance(population, _Set):
    population = tuple(population)
if isinstance(population, _Mapping):
    raise Blarrrgh()
# Otherwise assume that we have a sequence and hope

这也是关于现有ndarray子类的一个好点。 看起来似乎没有任何办法可以说ndarray是Sequence但是它的子类不是:-(。因此,鉴于这些子类中的某些不能满足Sequence ,哪个选项最不坏

  • 弃用ndarray子类中不兼容的用法,并最终将其删除,并用Sequence兼容版本替换。 这对于count方法似乎可行,但是更改Series.index将对熊猫人造成极大的破坏。 (DataFrame不是ndarray的子​​类,因此从技术上讲它是不相关的,除非我认为Series和DataFrame应该保持同步。)我猜我们可以问@wesm他的想法,但是...
  • 继续声明ndarray及其子类,以满足Sequence定义,并接受对于某些ndarray子类来说这是一个谎言。 不过,仅在Sequence接口的很少使用的部分上,Python类型通常还是存在的。
  • 重新调整我们的继承层次结构以分离功能和抽象基类废话。 做
class multidim_ndarray(ndarray, Sequence):
  pass

并改用此类的多维数组实例。 子类不会受到影响,因为它们继续从ndarray继承,而不是从multidim_ndarray继承。 当然,单个ndarray对象可以通过.resize()在0维和多维之间转换...

  • 接受ndarray永远不会是Sequence

[ isSequenceType有点让人分心。 那是一个古老的函数,它早于抽象基类(在2.6中添加)的存在,并且甚至没有试图确定序列所需的详细接口,它只是检查您的类型(1)定义了__getitem__ (2)不是内置的dict 。 显然,这在许多情况下都会给出错误的答案(例如,任何行为像字典,但不是一个!)。 因此,如果确实需要序列类型,那么isinstance(obj, Sequence)会做得更好,而2to3则做对了。 但这为numpy带来了问题...]

也许有可能说服python来创建一个像SequenceBase这样的新类,该类甚至低于Sequence,并且不能保证.index.count ,但只能保证.__len____getitem__等? Sequence具有类似index ,但是通过使其看起来像鸭子一样的序列来强制将其应用于诸如numpy类的东西似乎有点怪异。 蟒蛇人是否意识到这有点问题?

我喜欢@seberg的建议; 如果Python开发人员不同意,我会推荐@njsmith的第二个项目符号。 缺少的选择是仅说ndarrays不满足Sequence接口。 并非最佳,但比子弹1和3恕我直言更好。

[哎呀,“缺失选项”作为选项4出现了,只是降价解析器决定以一种混乱且难以理解的方式将其折叠到上一个项目符号中。 我已经编辑了注释以修复格式。]

使用Sequence注册的类型的一半(即bufferxrange )也没有这些方法。 对于我来说,尚不清楚这些是接口的必需方法,还是对于那些使用collections.Sequence作为基类/ mixin的用户来说是便捷方法。

@rkern :好收获。 因此,也许解决方案只是在某个地方添加对Sequence.register(np.ndarray)的调用。 (这对于原始报告者也是一种解决方法。)

我们也应该在某个时候实现__reversed__

@rkern,您是对的,在PEP中提到这是一个未解决的问题: http : //www.python.org/dev/peps/pep-3119/#sequences 状态为Final的PEP甚至可能有未解决的问题。

我认为该错误的标题有点误导,因为该错误仅在python3下不存在。 当然, random.sample(numpy_array)可以在python2中使用,但是isinstance(np.array([1]), collections.Sequence)应该在任何python> = 2.6中返回True

我刚刚在Python 2.7中使用autopep8模块遇到了此错误。 默认情况下,它将某些operator.isSequenceType()调用转换为isinstance(x,collections.Sequence)。 当我传入numpy.ndarray时,测试将变为False。 这可能是一个非常偷偷摸摸的错误。

使用python-pillow模块在Python 2.7中也遇到了它。 Image.point(lut,mode)调用isinstance(lut,collections.Sequence),先前版本使用过operator.isSequenceType()

现在可能是重新讨论此问题的好时机,因为已注册了numpy数字标量类型(#4547),

因此,也许解决方案就是在某个地方添加对Sequence.register(np.ndarray)的调用。

是的,这是一个很好的妥协。

是的,只需在某处添加Sequence.register(np.ndarray)

@mitar有兴趣提交PR吗?

当然。 这应该去哪里? 在创建np.ndarray的同一文件中?

只是要确保我们实际上认为这是个好主意:我们现在为False (#9718)添加一个空数组的弃用,即,我们删除了对序列有用的东西) 。 尽管阅读了评论,但我认为结论已经是数组标量将不起作用,因此我猜想空数组可能是该违约的一部分...

供将来参考,正确的位置可能在numpy.core.multiarray
https://github.com/numpy/numpy/blob/4f1541e1cb68beb3049a21cbdec6e3d30c2afbbb/numpy/core/multiarray.py

好吧,我想要这个。 怎么样? 这就是我将它们实现为方法的方式:

def __reversed__(self):
    return iter(self[::-1])

def index(self, value) -> int:
    return np.in1d(self, value).nonzero()[0]

def count(self, value) -> int:
    return (self == value).sum()

# Necessary due to lack of __subclasshook__
collections.abc.register(np.ndarray)

我们发现,在最新版本的Tensorflow(2.0)中,使用Sequence.register(np.ndarray)会使Tensorflow行为异常。 似乎正在检查某处值是否为序列,然后使用的值与ndarray是否不同。

参见: https :

搞笑我很确定测试某个东西是否为数组是更好的主意,因为它几乎总是特殊处理的情况。

类型检查的顺序可能是错误的,它应该首先检查ndarray,然后检查序列。 但是,如果您首先检查顺序,那么现在该代码块开始运行。

@mitar我们正在考虑关闭此方法,因为__contains__ / operator.in行为不同(它是递归的,并且不针对序列),因此它违反了API约定。 您有一个用例吗?

您能在这里详细说明一下API合同吗? 我不完全了解。

用例正在编写通用代码,该代码知道如何在事物之间进行转换,例如是否可以遍历序列并按维数获取维度,然后递归。 然后,我可以按照与2d ndarray相同的方式转换列表列表,但是可以将其推广到多个维度,依此类推。 而且,我不必只是检查它是一个序列而已。

如前所述,将数组视为嵌套的python序列存在两个问题。 __contains__是最明显的一个,另一个是0-D数组绝对不是嵌套序列。 还存在一些细微之处,例如长度为0的维,通常arr[0] = 0并不意味着arr[0] == 0 ,因为arr[0]本身可以是任意数组(最好拼写为arr[0, ...] 。就我个人而言,我认为“嵌套序列”的解释很好,但是没有我们通常认为的有用(即,我很少将数组迭代为for col in array ,即使这样做,我也不会介意写for col in array.iter(axis=0)

所以我倾向于看到“数组是一个序列”稍微有问题的比喻(这并不意味着它不能uesful,我承认)。
但是,无论用例如何,我都好奇探索新的ABC(例如新的“ ElementwiseContainer”)是否更好。 还会告诉用户+==等在每个元素上都可以工作,并且,与Python序列不同,他们不应该期望+进行串联(是的+不是Sequence ABC的一部分,但是在Python中感觉很自然。

刚刚经过-
我上周写信给Python-ideas,是因为我注意到Python的collections.abc.Sequence并没有实现__eq__和其他比较-尽管它具有所有其他方法来实现使Sequence像列表和元组。 (该邮件线程将我引导至此问题)。

我提议在此处添加__eq__ ,但这显然会使这些序列与Numpy.array的行为有所不同。

在Python中,如何进一步形式化呢?什么是“序列”,然后委派这些会因特殊情况而有所不同的东西-在此处添加collections.abc.ComparableSequence ? (并且由于上面提到了+用于关联,所以可能有其他名称暗示“比较后的结果为单个布尔值,并表现为串联的标量并乘以标量”)-即-Python行为清单和元组中的+* )。 因此,可以以至少一维numpy数组与之完全匹配的方式来规范Sequence规范。

关于什么是Python序列的这种形式化也可以帮助解决其他分歧,例如上文https://github.com/numpy/numpy/issues/2776#issuecomment -330865166中提到的分歧。

但是,我没有足够的动力去独自走这条路-但是,如果这是有道理的,我会很乐意帮助编写PEP并推动它通过。 (我只是想检查为什么序列没有创建__eq__ ,并且在我提出这个建议时可能对此有PR)

@jsbueno我的问题是我真的看不到额外的内容,或者在定义之间,这实际上对ndarray用户没有帮助。 我能想到的最好的是Collection ,它具有count()index() ,但这有用吗? 对于Python本身几乎没有或根本没有概念的事情,其他任何东西都将成为ABC。

我认为SymPy实际上更正确。 它遍历矩阵的所有元素,至少使它成为Collection
现在,我怀疑我们是否可以为此做很多事情,而且我甚至不确定所有元素的SymPy迭代是否超级有用(直观),但至少所有元素的迭代与__contains__ 。 请注意,这也意味着len(Matrix)是元素的数量,_not_ Matrix.shape[0]

除了一维数组之外,还有从上面重复很多次的风险,什么是numpy数组?:

  • Container :元素:heavy_check_mark:
  • Sized + Iterable个子数组(如果不是一维的话):问题:
  • Reversible :我们可以实现它,而不必担心。 :题:
  • count()index() :可以为元素实现(:heavy_check_mark :)
  • Sequence :可迭代数组

因此,即使是一些最基本的属性也会发生冲突。 NumPy可以是Container ,它知道如何执行.index().count() ,即Sequence但没有Iterable部分。 虽然它是一个独立Iterable但是属于subarrays
如果那看起来像是一团混乱,那我同意,但我认为这是设计使然。 唯一真正的解决方案是要么走SymPy路径,要么就不是成为Iterable的开始。 (我们不能走SymPy之路,我怀疑弃用__iter__有机会。)

就个人而言,我的期望是,除了Python集合外,一维数组之外的类数组是与众不同的野兽。 考虑迭代行为时,您需要一个MultidimensionalCollection来专门表示__contains____iter__之间的不匹配(但这有用吗?)。

当超出Sequence当前定义的范围时,我要重申一下,我认为ElementwiseCollection (运算符是元素运算符,而不是容器运算符,例如+ )是最定义numpy数组和所有类似数组的特征(请参阅数组编程)。 但是,它也是与Python本身完全无关的概念,有时甚至与Python本身不一致。

唯一的事情是将一维数组标记为序列,并且将一维数组标记为序列,因为它们没有子数组与元素的不匹配。 在这一点上,是的,当然没有为它们定义__eq__ ,并且没有像典型的python序列那样定义__nonzero__

感谢您的答复,对于在这里行驶了8年的旅行车,我再次表示歉意。 有了您的评论,在上次电子邮件交换后的几个小时,并与中间的另一个朋友聊天,我同意,这些事情大多数还是保留下来的。 将来的某个时候,Python可以选择对Sequence进行更正式的定义,而不是使用“现在无论什么collections.abc.Sequence实现”。

在阅读完您的上述评论后,我想补充一点,我认为您列出为“ Python序列的成因”的特征缺少最重要的功能,该特征使ndarray对我而言类似于列表和元组之类的序列:具有连续的索引-可以处理所有单个元素的空间。 但是我不认为将abc形式化在编码或静态类型提示中都没有任何实际价值。

@seberg这是一个很棒的简介。

此问题似乎与在期望SequenceIterableContainer上下文中使用ndarray Container 。 一种简单的方法是让ndarray上的成员公开廉价的视图,这些视图承诺并提供适当的接口并响应isinstance支票。 例如:

class ndarray(Generic[T]):
    def as_container(self) -> Container[T]:
        if self.ndim == 0:
            raise ValueError
        return ContainerView(self)  # correctly answers __len__, __iter__ etc.
    def as_subarray_iterable(self) -> Iterable[np.ndarray[T]]:
        if self.ndim <= 1:
            raise ValueError
        return SubarrayIterableView(self)
    def as_scalar_sequence(self) -> Sequence[T]:
        if self.ndim != 1:
            raise ValueError
        return ScalarView(self)
    def as_subarray_sequence(self) -> Sequence[np.ndarray[T]]:
        if self.ndim <= 1:
            raise ValueError
        return SubarraySequenceView(self)  # this view has to reinterpret __contains__ to do the expected thing.

用户会问自己需要什么,而不是ndarray承诺要对所有人都适用,而ndarray可以提供,而是以最便宜的方式提供。 如果不能,则引发异常。 通过将用户应该做的ndim (特别是在使用类型注释时)移动到ndarray简化了用户代码。

最后的注释应该是Sequence而不是Iterable吗?

@ eric-wieser是的! 谢谢。 你怎么认为?

好吧, as_subarray_sequence实际上是list(arr) :)

@ eric-wieser是的,我认为提供一种意见会便宜一些,但我不知道。

好吧, list(arr)只会产生len(arr)视图,如果您进行迭代,无论如何您最终都会产生该视图。

我仍然认为我们将太多的精力放在可以做什么上,而对于目前存在的问题还不够。 特别是,如果您知道自己具有类似ndarray的方法(我不同意0-D数组不是容器),那么上面给出的所有方法都非常易于实现。 因此,它们只有在具有标准化ABC的情况下才有用,并且在那种情况下,定义基本索引是numpy兼容并且可能包括.flat属性也就足够了。

最初的问题( random.sample停止工作)由于经过时间而显得无关紧要。 是的,它有点烦人,但可能甚至会变得更好,因为用户可能希望选择子数组元素。

我确信我们确实破坏了一些鸭子式​​的代码。 序列化可能会出现一些问题(我手边没有示例)。 而且许多此类代码在ABC上使用isinstance支票都不会有问题,但讨厌专门检查np.ndarray 。 我看不到向ndarray添加方法如何帮助实现这一点,我们需要一个新的ABC ,可能只需要.ndim属性,并且可能包含嵌套序列样式迭代。

像上面这样的方法作为与任何类似数组的消费者协议一起使用可能是合理的,但这是否就是我们要解决的问题:)? 他们似乎不喜欢典型的Python序列要公开的东西。

@ eric-wieser

您当然是对的,但是您可能不会遍历整个序列。 您可能只会挑选一些要素。

@seberg

我仍然认为我们将太多精力放在可以做什么上,而对于目前存在的问题还不够

我同意你的看法。 您在想什么样的问题? 我正在想象当numpy 1.10带有类型时,我有时会想要使用一维numpy数组作为序列。 如果我现在想这样做,我需要:

  • 检查它是一维的,然后
  • 调用cast告诉mypy这实际上是一个序列。

这就是为什么我想提供一种自动执行此操作的方法。 我也讨厌大型接口,但是在我看来,随着类型注释的流行,这类方法或裸函数将越来越普遍。 你怎么看?

(我不同意0-D数组不是容器)。

我不知道,但是目前您正在为这些筹集__len__ ,所以看来它们不像容器那样工作。 我认为如果将0-D数组传递给接受容器的函数,mypy报告错误将很有帮助。 如果创建0-D数组容器,它将不会捕获。

我们将需要一个新的ABC,可能仅具有.ndim属性,并且可能包含嵌套序列样式迭代。

我不想将其添加到我的建议中,但是无论如何,我认为这就是您要去的地方。 我是精心设计的JAX库的狂热用户。 我想将来, numpy.ndarrayjax.numpy.ndarray (具有子类)都将从某种抽象的NDArray继承。 您可能会超出ndim 。 理想情况下,至少应NDArray(Generic[T]) ,并且事件可能也具有形状或维数。 它可能有__eq__返回NDArray[np.bool_] 。 你可能比我更了解:)

几年前,我搜索了此问题以建议numpy.array应该从collections.Sequence继承,但是现在我发现此线程中的参数(尤其是您的!!)非常令人信服。 Numpy数组并不是真正的序列,将它们弄乱似乎会造成弊大于利。 为什么不仅仅让他们成为自己的东西,而迫使用户显式地请求他们想要的接口呢?

而且许多此类代码在ABC上使用isinstance检查都不会有问题,

既然您提到了,也许我提出的所有方法都应该返回视图。 这样,他们可以正确回答实例检查。

像上面这样的方法作为与任何类似数组的消费者协议一起使用可能是合理的,但这是否就是我们要解决的问题:)? 他们似乎不喜欢典型的Python序列要公开的东西。

我绝对同意,答案取决于我们要解决的问题。 喝完类型注释库尔辅助工具后,我对编写简洁的numpy代码感兴趣,该代码可以通过mypy传递# type: ignore而不乱扔代码。 您要考虑哪些问题?

好吧,类型提示和与其他类似数组的对象互操作可能是一个很好的动机。 我可能建议打开一个新的问题或邮件列表线程。 现在,我不确定最好在这里考虑什么,因为打字正在形成,所以也许最终会澄清一些事情。

此页面是否有帮助?
0 / 5 - 0 等级