一个Python函数,接受标量或NumPy数组作为参数。

15

正如标题所述,假设我想编写一个符号函数(现在先忘记sign(0)),显然我们期望sign(2) = 1以及sign(array([-2,-2,2])) = array([-1,-1,1])。然而,下面的函数并不能处理numpy数组。

def sign(x):
    if x>0: return 1
    else: return -1
下一个函数也行不通,因为如果 x 只是一个单独的数字,则它没有形状成员。即使使用某些技巧,如 y = x*0 + 1,y 也没有 [] 方法。
def sign(x):
    y = ones(x.shape)
    y[x<0] = -1
    return y
即使参考了另一个问题的想法(如何制作一个可以接受numpy数组、可迭代对象或标量的numpy函数?),下面的函数在x是单个数字时将无法工作,因为在这种情况下,x.shape和y.shape只是(),索引y是非法的。
def sign(x):
    x = asarray(x)
    y = ones(x.shape)
    y[x<0] = -1
    return y

唯一的解决方案似乎是首先确定x是数组还是数字,但我想知道是否有更好的方法。如果您有很多这样的小函数,编写带分支的代码将非常麻烦。


1
使用掩码对 y 进行索引是合法的:这里的问题在于 x < 0 再次成为标量而不是零维数组。如果您尝试 y[asarray(x < 0)],它应该可以工作。 - Mark Dickinson
你有没有考虑使用内置的 np.sign 函数的可能性? - Jaime
@MarkDickinson 这是一个好的代码,但是当x是单个数字时会出现错误,因为此时y也必须是单个数字,那么就无法进行索引操作了... - Taozi
@Jaime,符号函数只是举个例子,我应该使用数学中的分段函数。顺便问一下,你知道numpy中符号函数是如何实现的吗? - Taozi
@Taozi:没错;如果你想采用这种方法,仍然需要在开头加上 x = asarray(x) - Mark Dickinson
8个回答

4

np.vectorize可以用来实现向量化,但是速度较慢,因为当您的装饰函数被调用时,它只是循环遍历数组元素并将标量函数应用于每个元素,即没有利用numpy的速度。

我发现一种有用的向量化if-else函数的方法是使用np.choose

def sign_non_zero(x):
    return np.choose(
        x > 0,  # bool values, used as indices to the array
        [
            -1, # index=0=False, i.e. x<=0
            1,  # index=1=True, i.e. x>0
        ])

x是标量或数组时,它可以运行,并且比在Python空间循环更快。

使用np.choose的唯一缺点是,以这种方式编写if-else逻辑不直观,并且代码可读性较差。每次我使用它时,都会包含像上面那样的注释,以使读者更容易理解正在发生什么。


sign_non_zero([1,2,3]) 的输出结果为: 1 # 应该是 1,1,1 sign_non_zero([1,2,-3]) 的输出结果为: 1 # 应该是 1,1,-1 - Irshad Bhat
@BHATIRSHAD,没错,目前来看,sign_non_zero支持标量和numpy数组。为了也支持列表,你可以简单地将x替换为np.asarray(x) - shx2
1
@bhat-irshad 在将 x 替换为 np.asarray(x) 后,这个函数非常适合实现符号函数。但是,当结果为是或否时,使用 choose 函数似乎更方便。如果你需要做出三重决策(假设你考虑 sign(0)),那么 choose 函数就无用了,我们又不得不面对旧问题——如果 x 可以被索引,也就是说,x 是一个数字或数组。 - Taozi
@Taozi 如果你需要做出三重决策,我不会说*choose是无用的。在你的“标量版本”中,每个ifelif语句都可以转换为对choose的调用,因此你需要使用两次choose。就if语句数量而言,你的标量版本的复杂度在选择版本中得以保留,即选择调用的数量。这仍然具有numpy快速的优点。 - shx2
@shx2 我得出结论,我想要实现的是不可能的。其实这很好,因为大多数情况下,你处理数组还是非数组都非常清楚。问题出在当我在玩指数分布 f(x) = {0:(x<0); exp(-x):(otherwise)} 时,它是分段函数。我会用较困难的方式来处理它。 - Taozi
1
不错的使用了 np.choose。你也可以看看 np.select,它支持更复杂的返回类型。 - goofd

3

我想知道你是否需要一个向量化函数

>>> import numpy as NP

>>> def fnx(a):
        if a > 0:
            return 1
        else:
            return -1

>>> vfnx = NP.vectorize(fnx)

>>> a = NP.random.randint(1, 10, 5)
array([4, 9, 7, 9, 2])

>>> a0 = 7

>>> vfnx(a)
array([1, 1, 1, 1])

>>> vfnx(a0)
array(1)

这很好,但是shx2所说的向量化函数慢且不能充分利用numpy的速度,这是真的吗?此外,如果使用此方法,每个函数都需要定义两次 - 一个是专注于单个数字的简单版本,另一个是向量化版本,其名称应该接近但不同,这正确吗? - Taozi
根据文档,向量化函数是作为Python for循环实现的,事实上,NumPy性能的主要原因是面向数组的计算(在C源代码中只有一个for循环),避免了第二个Python for循环。但您不需要第二个函数;向量化函数的目的是在单个函数调用中处理NumPy数组和标量,使用相同的函数。 - doug

3
这里有一个解决方案:
import numpy as np

def sign(x):
    y = np.ones_like(x)
    y[np.asarray(x) < 0] = -1

    if isinstance(x, np.ndarray):
        return y
    else:
        return type(x)(y)

这应该返回与输入相同类型的值。例如:sign(42) 返回值为 1sign(42.0) 返回值为 1.0。如果给出一个 ndarray,则它将像 np.sign 一样工作。

通常,您可以假设输入是一个 ndarray。如果您尝试访问 ndarray 具有但输入没有的属性或方法,则会回退到操作标量类型。使用异常来实现此功能。例如:

def foo_on_scalars(x):
    # do scalar things

def foo(x):
    try:
        # assume x is an ndarray
    except AttributeError:
        foo_on_scalars(x)

1
numpy函数可以自然地处理标量或数组输入,并保留输出中的形状。因此,最好找到执行任务的numpy函数。在这种情况下,建议使用函数np.sign。对于不同的逻辑,您可以使用np.where(x>0, 1, -1),它适用于x的标量和数组值。

0

处理标量和numpy数组的简单解决方案:

>>> import numpy as np

>>> def sign_non_zero(x):
        return (x > 0) * 1 + (x < 0) * -1

>>> sign_non_zero(2)
1

>>> sign_non_zero(np.array([-2, -2, 2]))
array([-1, -1,  1])

0
我之前采用的方法很像你最后一个例子,但是在开头添加了一个额外的标量检查:
def sign(x):
    if isscalar(x):
        x = (x,)
    x = asarray(x)
    y = ones(x.shape)
    y[x<0] = -1
    return y

0
这里有一个解决方案:
>>> def sign(x):
...      if type(x)==int:
...          if x>0: return 1
...          else: return -1 
...      else:
...          x=np.array(x)
...          pos=np.where(x>=0)
...          neg=np.where(x<0)
...          res=np.zeros(x.shape[0])
...          res[pos]=1
...          res[neg]=-1
...          return res.tolist()
... 
>>> sign(56)
1
>>> sign(-556)
-1
>>> sign([23,4,-3,0,45,-3])
[1.0, 1.0, -1.0, 1.0, 1.0, -1.0]
>>> sign(np.array([23,4,-3,0,45,-3]))
[1.0, 1.0, -1.0, 1.0, 1.0, -1.0]

1
sign(56L)的输出是什么?或者sign(np.int32(56))sign(56.)呢?此外,整个重点在于避免逻辑的重复。 - shx2
@shx2 正是我想问的,类型判断的问题在于有太多的类型。可以使用if type(numpyarray) == 'ndarray',但我想避免这些分支。 - Taozi

0

你可以先将数字转换为单元素数组,

然后集中处理数组。

不过你仍然需要检查 x 的类型。


但是函数返回一个单元素数组,需要从客户端进行解包。 - Taozi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接