Функция 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