Написание функций, которые принимают как одномерные, так и двумерные массивы? - PullRequest
15 голосов
/ 27 ноября 2011

Насколько я понимаю, одномерные массивы в numpy можно интерпретировать как вектор, ориентированный на столбцы, или вектор, ориентированный на строки.Например, 1-D массив с формой (8,) можно рассматривать как 2-D массив с формой (1,8) или формой (8,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 ...

То есть я могу обобщить код для обработки двумерных случаев (r,1), (1,c), (r,c), но не в одномерных случаях без разветвления или изменения формы.

Становится еще страшнее, когда функция работает с несколькими массивами, как я бы проверял и преобразовывал каждый аргумент.

Так что мой вопрос: я скучаю по лучшему идиому?Является ли шаблон, который я описал выше, общим для кода NumPy?

Кроме того, в качестве связанного вопроса принципов разработки API, если вызывающий объект передает 1-D массив какой-либо функции, которая возвращает новый массив, ивозвращаемое значение также является вектором. Является ли обычной практикой преобразование двумерного вектора (r,1) или (1,c) обратно в одномерный массив или просто документ, подтверждающий, что функция возвращает двумерный массив независимо от этого?

Спасибо

Ответы [ 4 ]

6 голосов
/ 27 ноября 2011

Я думаю, что в общем случае функции NumPy, для которых требуется массив формы (r,c), не допускают специального учета для одномерных массивов.Вместо этого они ожидают, что пользователь либо пропустит массив формы (r,c) точно, либо пользователь передаст 1-D массив, который передает до формы (r,c).

Если вы передадите такую ​​функцию 1-D массиву формы (c,), он будет транслироваться в форме (1,c), поскольку трансляция добавляет новые оси слеваОн также может транслировать в форме (r,c) для произвольного r (в зависимости от того, с каким другим массивом он объединяется).

С другой стороны, если у вас есть одномерный массив x, имеющий форму (r,), и вам нужно, чтобы он транслировался до формы (r,c), тогда NumPy ожидает, что пользователь передастмассив формы (r,1), так как широковещание не добавит новые оси справа для вас.

Чтобы сделать это, пользователь должен передать x[:,np.newaxis] вместо x.


Относительно возвращаемых значений: я думаю, что лучше всегда возвращать двумерный массив.Если пользователь знает, что вывод будет иметь форму (1,c), и ему нужен 1-D массив, пусть она сама отрежет 1-D массив x[0].

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

Кроме того, радиовещание стирает различие между 1-D массивом формы (c,) и 2-D массивом формы (r,c).Если ваша функция возвращает 1-D массив при подаче 1-D ввода и 2-D массив при подаче 2-D ввода, то ваша функция делает различие строгим, а не размытым.Стилистически это напоминает мне о проверке if isinstance(obj,type), которая идет вразрез с типизацией утки.Не делай этого, если не нужно.

5 голосов
/ 29 ноября 2011

Объяснение unutbu хорошо, но я не согласен с возвращаемым измерением.

Внутренний шаблон функции зависит от типа функции.

Операции сокращения с аргументом оси часто можно записать так, чтобы количество измерений не имело значения.

Numpy также имеет функцию atleast_2d (и atleast_1d), которая также обычно используется, если вынужен явный 2d массив.В статистике я иногда использую функцию, подобную atleast_2d_cols, которая преобразует 1d (r,) в 2d (r, 1) для кода, который ожидает 2d, или если входной массив равен 1d, то для интерпретации и линейной алгебры требуется вектор столбца.(изменение формы дёшево, так что это не проблема)

В третьем случае у меня могут быть разные пути кода, если нижний размерный случай может быть сделан дешевле или проще, чем более высокий размерный случай.(пример: если для 2d требуется несколько точечных произведений.)

возвращаемое измерение

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

Для многих других функций выходное измерение соответствует входному измерению.Я думаю, что 1d вход должен иметь 1d выход, а не лишнее измерение.За исключением функций в linalg, я не помню функций, которые бы возвращали избыточное дополнительное измерение.(Случай скалярного и одноэлементного массива не всегда согласован.)

Стилистически это напоминает мне проверку isinstance:

Попробуйте без него, если вы разрешите, например, для пустых матриц и замаскированных массивов,Вы получите забавные результаты, которые нелегко отладить.Хотя для большинства функций 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)
2 голосов
/ 05 марта 2017

На этот вопрос уже есть очень хорошие ответы.Здесь я просто хочу добавить то, что я обычно делаю (что каким-то образом суммирует ответы других), когда я хочу написать функции, которые принимают широкий диапазон входных данных, в то время как для операций, которые я выполняю над ними, требуется двухмерный вектор строки или столбца.

  1. Если я знаю, что ввод всегда 1d (массив или список):

    a.если мне нужен ряд: x = np.asarray(x)[None,:]

    б.если мне нужен столбец: x = np.asarray(x)[:,None]

  2. Если ввод может быть либо 2d (массив или список) с правильной формой, либо 1d (который необходимо преобразовать в 2d строку / столбец):

    a.если мне нужна строка: x = np.atleast_2d(x)

    б.если мне нужен столбец: x = np.atleast_2d(np.asarray(x).T).T или x = np.reshape(x, (len(x),-1)) (последний кажется быстрее)

2 голосов
/ 04 июля 2016

Это хорошее применение для декораторов

def atmost_2d(func):
  def wrapr(x):
    return func(np.atleast_2d(x)).squeeze()
  return wrapr

Например, эта функция выберет последний столбец своего ввода.

@atmost_2d
def g(x):
  return x[:,-1]

Но: это работает для:

1д:

In [46]: b
Out[46]: array([0, 1, 2, 3, 4, 5])

In [47]: g(b)
Out[47]: array(5)

2d:

In [49]: A
Out[49]:
array([[0, 1],
       [2, 3],
       [4, 5]])

In [50]: g(A)
Out[50]: array([1, 3, 5])

0d:

In [51]: g(99)
Out[51]: array(99)

Этот ответ основан на двух предыдущих.

...