Функция Python, которая обрабатывает скалярные или массивы

Как лучше написать функцию, которая может принимать либо скалярные поплавки, либо числовые векторы (1-й массив), и возвращать скалярный, 1-й массив или 2-й массив, в зависимости от ввода?

Функция является дорогостоящей и часто называется, и я не хочу возлагать бремя на вызывающего, чтобы делать специальные приведения к аргументам или возвращать значения. Это нужно только для обработки чисел (не списков или других повторяющихся вещей).

np.vectorize может быть медленным (Передача функции python на массивы numpy) и другие ответы (Получение Python функция, чтобы чисто вернуть скаляр или список, в зависимости от количества аргументов) и np.asarray(Функция python, которая принимает в качестве аргумента скаляр или массив numpy) не помогает в получении размеров, необходимых для выходного массива.

Этот тип кода будет работать в Matlab, Javascript и других языках:

import numpy as np

def func( xa, ya ):
    # naively, I thought I could do:
    xy = np.zeros( ( len(xa), len(ya) ) )
    for j in range(len( ya )):
        for i in range(len( xa )):
            # do something complicated
            xy[i,j] = x[i]+y[j]            
    return xy

Прекрасно работает для массивов:

x = np.array([1., 2.])
y = np.array([2., 4.])
xy = func(x,y)
print xy

[[ 3.  5.]
 [ 4.  6.]]

Но не работает для скалярных поплавков:

x = 1.
y = 3.
xy = func(x,y)
print xy

<ipython-input-64-0f85ad330609> in func(xa, ya)
      4 def func( xa, ya ):
      5     # naively, I thought I could do:
----> 6     xy = np.zeros( ( len(xa), len(ya) ) )
      7     for j in range(len( ya )):
      8         for i in range(len( xa )):

TypeError: object of type 'float' has no len()

Использование np.asarray в подобной функции дает:

<ipython-input-63-9ae8e50196e1> in func(x, y)
      5     xa = np.asarray( x );
      6     ya = np.asarray( y );
----> 7     xy = np.zeros( ( len(xa), len(ya) ) )
      8     for j in range(len( ya )):
      9         for i in range(len( xa )):

TypeError: len() of unsized object

Каков быстрый, элегантный и питонический подход?

Ответ 1

Всюду по базе кода numpy вы найдете такие вещи, как:

def func_for_scalars_or_vectors(x):
    x = np.asarray(x)
    scalar_input = False
    if x.ndim == 0:
        x = x[None]  # Makes x 1D
        scalar_input = True

    # The magic happens here

    if scalar_input:
        return np.squeeze(ret)
    return ret

Ответ 2

", которая может принимать либо скалярные поплавки, либо числовые векторы (массив 1-d), и возвращать скаляр, 1-й массив или 2-й массив"

So

скаляр = > скаляр

1d = > 2d

что создает 1-мерный массив?

def func( xa, ya ):
    def something_complicated(x, y):
        return x + y
    try:
        xy = np.zeros( ( len(xa), len(ya) ) )
        for j in range(len( ya )):
            for i in range(len( xa )):
                xy[i,j] = something_complicated(xa[i], ya[i])
    except TypeError:
        xy = something_complicated(xa, ya)  
    return xy

Является ли это "быстрым, элегантным и питоническим"?

Это, безусловно, "пифонический". 'try/except' очень Pythonic. Таким образом, определяется функция внутри другой функции.

Быстро? Только тесты времени покажут. Это может зависеть от относительной частоты скалярных примеров массива v.

Elegant? То есть в глазах смотрящего.

Является ли это более элегантным? Это ограниченная рекурсия

def func( xa, ya ):
    try:
        shape = len(xa), len(ya)
    except TypeError:
        # do something complicated
        return xa+ya    
    xy = np.zeros(shape)
    for j in range(len( ya )):
        for i in range(len( xa )):
            xy[i,j] = func(xa[i], ya[i])           
    return xy

Если вам нужно правильно обрабатывать входы 2d +, то vectorize, безусловно, является наименее затратным решением:

def something_complicated(x,y):
    return x+y
vsomething=np.vectorize(something_complicated)

In [409]: vsomething([1,2],[4,4])
Out[409]: array([5, 6])
In [410]: vsomething(1,3)
Out[410]: array(4)   # not quite a scalar

Если array(4) не является выводом scalar, который вы хотите, тогда вам нужно будет добавить тест и извлечь значение с помощью [()]. vectorize также обрабатывает сочетание скаляра и массива (скаляр + 1d = > 1d).

MATLAB не имеет скаляров. size(3) возвращает 1,1.

В Javascript [1,2,3] имеет атрибут .length, но 3 не работает.

из сеанса nodejs:

> x.length
undefined
> x=[1,2,3]
[ 1, 2, 3 ]
> x.length
3

Что касается кода MATAB, Octave имеет это сказать о функции length

- Встроенная функция: длина (A)      Верните длину объекта A.

Длина равна 0 для пустых объектов, 1 для скаляров и числу      элементы для векторов. Для объектов матрицы длина - это число      строк или столбцов, в зависимости от того, что больше (это нечетное определение      используется для совместимости с MATLAB).

MATLAB не имеет истинных скаляров. Все по крайней мере 2d. "Вектор" просто имеет размерность "1". length - плохой выбор для управления итерацией в MATLAB. Я всегда использовал size.

Чтобы добавить к MATLAB удобство, но также и потенциальную путаницу, x(i) работает как с векторами строк, так и с векторами столбцов, [1,2,3] и [1;2;3]. x(i,j) также работает с обоими, но с разными диапазонами индексов.

len отлично работает при итерации списков Python, но это не лучший выбор при работе с массивами numpy. x.size лучше, если вы хотите общее количество элементов. x.shape[0] лучше, если вы хотите 1-го измерения.

Частично, почему нет изящного решения Pythonic для вашей проблемы, вы начинаете с того, что является идиоматическим MATLAB, и ожидал, что Python будет вести себя со всеми теми же нюансами.

Ответ 3

Как бы то ни было, я бы предпочел, чтобы функция была гибкой по типам ввода, но всегда возвращала согласованный тип; это то, что в конечном итоге не позволит вызывающим абонентам проверять типы возврата (заявленная цель).

Например, разрешите скаляры и/или массивы в качестве аргументов, но всегда возвращайте массив.

def func(x, y):
    # allow (x=1,y=2) OR (x=[1,2], y=[3,4]) OR (!) (x=1,y=[2,3])
    xn = np.asarray([x]) if np.isscalar(x) else np.asarray(x)
    yn = np.asarray([y]) if np.isscalar(y) else np.asarray(y)

    # calculations with numpy arrays xn and xy
    res = xn + yn  # ..etc...
    return res

(Тем не менее, пример можно легко изменить, чтобы вернуть скаляр, установив флаг "scalar=True", yada yada yada.., но вам также придется обрабатывать один arg скаляр, другой - массив, и т.д., мне кажется много YAGNI.)

Ответ 4

Я бы сделал следующее:

def func( xa, ya ):
    xalen = xa if type(xa) is not list else len(xa)
    yalen = ya if type(ya) is not list else len(ya)
    xy = np.zeros( (xalen, yalen) )    
    for j in range(yalen): 
        for i in range(xalen):
            xy[i,j] = x[i]+y[j] 
    return xy

Ответ 5

Возможно, это не самый пифонический (и не самый быстрый), но это самый беспутный способ:

import numpy as np

def func(xa, ya):
    xa, ya = map(np.atleast_1d, (xa, ya))
    # Naively, I thought I could do:
    xy = np.zeros((len(xa), len(ya)))
    for j in range(len(ya)):
        for i in range(len(xa)):
            # Do something complicated.
            xy[i,j] = xa[i] + ya[j]
    return xy.squeeze()

Если вы ищете проверку скорости numba вне.

Ответ 6

Напишите свою функцию, чтобы не заботиться о размерности в первую очередь:

def func(xa, ya):
    # use x.shape, not len(x)
    xy = np.zeros(xa.shape + ya.shape)

    # use ndindex, not range
    for jj in np.ndindex(ya.shape):
        for ii in np.ndindex(xa.shape):
            # do something complicated
            xy[ii + jj] = x[ii] + y[jj]            
    return xy

Ответ 7

Это похоже на то, что может сделать декоратор функции, если вы хотите применить это поведение ко многим функциям, которые вы пишете, как я. Я написал один. Оказалось, что это сложнее, чем я надеялся, но вот оно.

Конечно, мы все должны писать код, который явно принимает и возвращает только массивы или скаляры. Явное лучше, чем неявное. Используйте с осторожностью.

import inspect
import functools
import numpy as np

def scalar_or_array(*names):
    """
    Decorator to make a function take either scalar or array input and return
    either scalar or array accordingly.

    The decorator accepts as arguments the names of all the parameters that
    should  be turned into arrays if the user provides scalars. Names should
    be strings.

    The function must be able to handle array input for all of the named
    arguments.

    In  operation, if all the  named arguments are scalars, then the
    decorator will apply np.squeeze() to everything the function returns.


    Example:
    @mnp.scalar_or_array('x', 'y')
    def test(x, y):
    x[x > 10] = 0
        return x, y

    test(5, 0)
    # returns: (array(5), array(0))

    test(20, 0)
    # returns: (array(0), array(0))

    test(np.arange(20), 0)
    # returns: (array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10,  0,  0,  0,
                        0,  0,  0,  0,  0,  0]), array([0]))
    # notice that the second value returned gets turned into an array in
    # this case
    """
    def decorator(func):

        # get the decorated functions call signature
        signature = inspect.signature(func)

        # now the modified version of the decorated function
        @functools.wraps(func)
        def mod(*args, **kwargs):
            # map this modified function arguments to that of the decorated
            # function through the "bind" method of the signature
            boundargs = signature.bind(*args, **kwargs)

            # now check if each of the listed arguments is a scalar. if so,
            # make it an array with ndim=1 in the bound arguments.
            scalar_input = []
            for name in names:
                if name in signature.parameters:
                    val = boundargs.arguments[name]
                    if np.isscalar(val):
                        scalar_input.append(True)
                        ary = np.reshape(val, 1)
                        boundargs.arguments[name] = ary
                    else:
                        scalar_input.append(False)

            # now apply the function
            result = func(**boundargs.arguments)

            # if all the user-named inputs were scalars, then try to return
            # all scalars, else, just return what the functon spit  out
            if all(scalar_input):
                if type(result) is tuple:
                    return tuple(map(np.squeeze, result))
                else:
                    return np.squeeze(result)
            else:
                return result

        return mod
    return decorator