编写接受一维和二维numpy数组的函数?

17 投票
4 回答
4814 浏览
提问于 2025-04-17 07:09

我理解的是,在numpy中,1维数组可以被看作是列向量或行向量。举个例子,一个形状为 (8,) 的1维数组可以根据上下文被视为形状为 (1,8) 的2维数组,或者形状为 (8,1) 的2维数组。

我遇到的问题是,我写的处理数组的函数在处理2维数组时通常能很好地适应行向量和矩阵,但在处理1维数组时就不太灵活了。

因此,我的函数最终会做一些像这样的事情:

if arr.ndim == 1:
    # Do it this way
else:
    # Do it that way

甚至可能是这样:

# Reshape the 1-D array to a 2-D array
if arr.ndim == 1:
    arr = arr.reshape((1, arr.shape[0]))

# ... Do it the 2-D way ...

也就是说,我发现我的代码可以很好地处理2维情况 (r,1)(1,c)(r,c),但在处理1维情况时就需要分支或重新调整形状。

当函数需要处理多个数组时,情况会变得更加复杂,因为我需要检查并转换每个参数。

所以我的问题是:我是不是漏掉了什么更好的写法?我上面描述的这种情况在numpy代码中常见吗?

另外,关于API设计原则,如果调用者传递一个1维数组给某个函数,而这个函数返回一个新数组,并且返回值也是一个向量,通常的做法是将2维向量 (r,1)(1,c) 重新调整为1维数组,还是直接说明这个函数返回的是2维数组呢?

谢谢

4 个回答

4

这个问题已经有很好的答案了。我想补充一下我通常的做法(这也算是对其他人回答的总结),当我想写一些可以接受多种输入的函数,而我对这些输入的操作需要用到二维的行向量或列向量时。

  1. 如果我知道输入总是是一维的(数组或列表):

    a. 如果我需要行向量:x = np.asarray(x)[None,:]

    b. 如果我需要列向量:x = np.asarray(x)[:,None]

  2. 如果输入可以是二维的(数组或列表)且形状正确,或者是一维的(需要转换为二维的行/列向量):

    a. 如果我需要行向量:x = np.atleast_2d(x)

    b. 如果我需要列向量:x = np.atleast_2d(np.asarray(x).T).T 或者 x = np.reshape(x, (len(x),-1))(后者似乎更快)

5

unutbu的解释很好,但我对返回的维度有不同看法。

函数内部的处理方式取决于函数的类型。

带有轴参数的归约操作通常可以写成不管维度多少都能工作的样子。

Numpy还有一个叫做至少2维(atleast_2d)和至少1维(atleast_1d)的函数,如果你需要明确的2维数组,这些函数也很常用。在统计学中,我有时会用一个像至少2维列(atleast_2d_cols)这样的函数,它可以把1维的数组(r,)变成2维的(r,1),这样代码就能处理2维的情况,或者如果输入数组是1维的,那么在解释和线性代数中就需要一个列向量。(重新调整维度的成本很低,所以这不是问题)

在第三种情况下,如果低维的处理方式比高维的更便宜或简单,我可能会有不同的代码路径。(例如:如果2维需要多个点积运算。)

返回的维度

我觉得不遵循numpy的返回维度约定会让用户感到困惑,尤其是对于一些通用函数。(特定主题的函数可能会有所不同。)例如,归约操作会减少一个维度。

对于许多其他函数,输出的维度和输入的维度是匹配的。我认为1维的输入应该有1维的输出,而不是多出一个冗余的维度。除了线性代数(linalg)中的函数,我不记得还有哪个函数会返回多余的维度。(标量和1元素数组的情况并不总是一致。)

从风格上讲,这让我想起了一个类型检查:

如果你允许使用numpy矩阵和掩码数组,可以试着不使用它。你会得到一些奇怪的结果,这些结果不容易调试。不过,对于大多数numpy和scipy的函数,用户必须知道数组类型是否适用,因为很少有类型检查,而asarray可能并不总是能做到正确的事情。

作为用户,我总是知道我拥有的是什么类型的“类似数组”的东西,比如列表、元组或者哪个数组子类,特别是在我使用乘法的时候。

np.array(np.eye(3).tolist()*3)
np.matrix(range(3)) * np.eye(3)
np.arange(3) * np.eye(3)

另一个例子:这段代码是干什么的?

>>> x = np.array(tuple(range(3)), [('',int)]*3)
>>> x
array((0, 1, 2), 
      dtype=[('f0', '<i4'), ('f1', '<i4'), ('f2', '<i4')])
>>> x * np.eye(3)
7

一般来说,NumPy的函数如果需要一个形状为 (r,c) 的数组,它们不会特别处理一维数组。也就是说,用户需要提供一个确切形状为 (r,c) 的数组,或者提供一个可以“广播”到 (r,c) 形状的一维数组。

如果你给这样的函数传入一个形状为 (c,) 的一维数组,它会被“广播”成形状为 (1,c) 的数组,因为广播是在左边添加新的维度。它也可以根据其他数组的形状,将其广播到 (r,c) 的形状,r 可以是任意值。

另一方面,如果你有一个形状为 (r,) 的一维数组 x,并且你想把它广播到形状 (r,c),那么NumPy希望用户提供一个形状为 (r,1) 的数组,因为广播不会在右边为你添加新的维度。

为了做到这一点,用户必须传入 x[:,np.newaxis],而不是直接使用 x


关于返回值:我认为最好总是返回一个二维数组。如果用户知道输出会是形状为 (1,c),并且想要一个一维数组,那就让她自己切片得到一维数组 x[0]

通过让返回值的形状始终相同,使用这个函数的代码会更容易理解,因为输入的形状并不总是显而易见。

另外,广播模糊了一维数组 (c,) 和二维数组 (r,c) 之间的区别。如果你的函数在接收到一维输入时返回一维数组,而在接收到二维输入时返回二维数组,那么你的函数就严格区分了这两者,而不是模糊处理。从风格上讲,这让我想起了检查 if isinstance(obj,type),这与鸭子类型的理念相悖。如果没有必要,就不要这样做。

撰写回答