编写接受一维和二维numpy数组的函数?
我理解的是,在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 个回答
这个问题已经有很好的答案了。我想补充一下我通常的做法(这也算是对其他人回答的总结),当我想写一些可以接受多种输入的函数,而我对这些输入的操作需要用到二维的行向量或列向量时。
如果我知道输入总是是一维的(数组或列表):
a. 如果我需要行向量:
x = np.asarray(x)[None,:]
b. 如果我需要列向量:
x = np.asarray(x)[:,None]
如果输入可以是二维的(数组或列表)且形状正确,或者是一维的(需要转换为二维的行/列向量):
a. 如果我需要行向量:
x = np.atleast_2d(x)
b. 如果我需要列向量:
x = np.atleast_2d(np.asarray(x).T).T
或者x = np.reshape(x, (len(x),-1))
(后者似乎更快)
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)
一般来说,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)
,这与鸭子类型的理念相悖。如果没有必要,就不要这样做。