Реализация 2D-нарезки в Python

Я реализую библиотеку линейной алгебры на Python (я знаю, что что-то, вероятно, существует, но я делаю это, чтобы узнать о Python и математике, которая мне нужна для моих экзаменов), и я хочу иметь доступ к элементам/подмножествам матрица такая:

(Мой матричный класс является подклассом кортежа.)

  • M = Matrix([list of rows of elements])
  • M[1, 2] Получает элемент в (1, 2)
  • M[3] Получает строку 3

Это довольно легко сделать, но я также хочу реализовать нарезку, например:

  • M[:,:] возвращает всю матрицу
  • M[1:6:2] возвращает строки 1, 3 и 5
  • M[1:6:2, 0:2] возвращает матрицу, состоящую из строк 1, 3 и 5, пересекающихся с первыми двумя столбцами.

Я сделал это, но мой ответ кажется очень непитоновским:

def __getitem__ (self, idx):
    if isinstance(idx, numbers.Integral):
        # Code to return the row at idx
    elif (isinstance(idx, tuple) and len(idx) == 2 and
            all(isinstance(i, numbers.Integral) for i in idx)):
        # Code to return element at idx
    elif (isinstance(idx, tuple) and len(idx) == 2 and
            all(isinstance(i, slice) for i in idx)):
        # Code to parse slices

Другая проблема заключается в том, что оба индекса должны быть числами или срезами, которые я не могу смешивать. Чтобы сделать это таким образом, потребуются еще два блока elif, а это похоже на два множества. Уже код действительно уродлив.

Я думаю, что ответ включает в себя утиную печать, но я не совсем уверен, как это реализовать. Я смотрел на блоки try:except:, но я не уверен, как связать их в цепочку, и я действительно не хочу слишком много вкладывать друг в друга.

Итак, ТАК, спасибо за чтение. Каков наилучший способ реализации такой функции?


person Max Bucknell    schedule 27.03.2013    source источник
comment
PS, как только вы закончите создавать это самостоятельно, посмотрите на numpy, самое заметное из того, что уже существует. Он делает все, что вы хотите, и даже больше, и, вероятно, сделает ваш код немного легче для чтения и намного быстрее. Я не хотел упоминать об этом сразу, потому что не хотел отговаривать вас от веселья, придумывая свои собственные проекты… но я также хочу убедиться, что вы не пропустите удовольствие от игры с numpy .   -  person abarnert    schedule 28.03.2013
comment
Нет, я знаю о numpy. Еще не использовал его, так как пишу на Python всего около трех дней. Тем не менее, это все, что я делал. Я получаю степень по математике, поэтому склоняюсь к этому. Спасибо еще раз!   -  person Max Bucknell    schedule 28.03.2013


Ответы (1)


Вы должны сделать что-то подобное… но, по крайней мере, вы можете удалить некоторое дублирование.

Во-первых, вероятно, разумно считать, что [1,] означает «строку 1», как и [1]. (numpy делает это.) Это означает, что вам не нужна вещь tuple-vs.-int; просто рассматривайте int как кортеж из 1 элемента. Другими словами:

def __getitem__(self, idx):
    if isinstance(idx, numbers.Integral):
        idx = (idx, slice(None, None, None))
    # now the rest of your code only needs to handle tuples

Во-вторых, хотя ваш пример кода обрабатывает только случай двух срезов, ваш реальный код должен обрабатывать два среза, или срез и целое число, или целое число и срез, или два целых числа, или срез, или целое число. Если вы можете выделить код обработки слайсов, вам не нужно будет дублировать его снова и снова.

Один из приемов обработки int-vs.-slice заключается в том, чтобы рассматривать [n] как оболочку, которая, по сути, делает [n:n+1][0], что позволяет еще больше сократить все. (Это немного сложнее, чем это, потому что вы должны использовать в особом случае либо отрицательные числа в целом, либо просто -1, потому что очевидно n[-1] != n[-1:0][0].) Для одномерных массивов это может быть не стоит, но для двумерных массивов это, вероятно, , потому что это означает, что когда вы имеете дело со столбцом, у вас всегда есть список строк, а не просто строка.

С другой стороны, вы можете захотеть поделиться некоторым кодом между __getitem__ и __setitem__… что делает некоторые из этих трюков либо невозможными, либо намного сложнее. Итак, есть компромисс.

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

class Matrix(object):
    def __init__(self):
        self.m = [[row + col/10. for col in range(4)] for row in range(4)]
    def __getitem__(self, idx):
        if isinstance(idx, (numbers.Integral, slice)):
            idx = (idx, slice(None, None, None))
        elif len(idx) == 1:
            idx = (idx[0], slice(None, None, None))
        rowidx, colidx = idx
        rowslice, colslice = True, True
        if isinstance(rowidx, numbers.Integral):
            rowidx, rowslice = slice(rowidx, rowidx+1), False
        if isinstance(colidx, numbers.Integral):
            colidx, colslice = slice(colidx, colidx+1), False
        ret = self.m[rowidx][colidx]
        if not colslice:
            ret = [row[0] for row in ret]
        if not rowslice:
            ret = ret[0]
        return ret

Или было бы лучше, если бы вы реорганизовали вещи по другой оси: получите строку (строки), а затем получите столбец (столбцы) внутри нее / них:

def _getrow(self, idx):
    return self.m[idx]

def __getitem__(self, idx):
    if isinstance(idx, (numbers.Integral, slice)):
        return self._getrow(idx)
    rowidx, colidx = idx
    if isinstance(rowidx, numbers.Integral):
        return self._getrow(rowidx)[colidx]
    else:
        return [row[colidx] for row in self._getrow(rowidx)]

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


Если вы возражаете против необходимости переключения типа в параметре index, да, в целом это кажется непитоновским, но, к сожалению, так обычно работает __getitem__. Если вы хотите использовать обычную логику EAFTP try, вы можете это сделать, но я не думаю, что это более читабельно, когда вам нужно попробовать два разных API (например, [0] для кортежей и .start для слайсов) в нескольких местах. В конечном итоге вы выполняете «переключение типа утки» вверху, например так:

try:
    idx[0]
except AttributeError:
    idx = (idx, slice(None, None, None))

… и так далее, и это вдвое больше кода, чем обычное переключение типов без каких-либо обычных преимуществ.

person abarnert    schedule 27.03.2013
comment
Красиво, на самом деле я был очень близок к вашему второму ответу с тем, что у меня было; большое тебе спасибо. Мне не нужно беспокоиться о setitem или внутренних методах хранения, потому что я наследую от кортежа. (Я также написал векторный класс, моя матрица представляет собой украшенный кортеж векторов, которые также являются украшенными кортежами.) - person Max Bucknell; 28.03.2013
comment
Использование кортежа 1D (длиной N*M) в качестве внутреннего хранилища определенно разумно, но вы можете рассмотреть возможность инкапсуляции и делегирования вместо наследования. - person abarnert; 28.03.2013