4

我正在用 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 行组成的矩阵。

我已经这样做了,但我的回答似乎非常不符合 Python 风格:

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:块,但我不确定如何将它们链接起来,而且我真的不想嵌套太多。

所以,所以,谢谢你的阅读。实现这样的功能的最佳方法是什么?

4

1 回答 1

4

你几乎必须做这样的事情......但至少你可以删除一些重复。

首先,考虑[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,或者一个 int 和一个切片,或者两个 int,或者一个切片,或者一个 int。如果您可以分解出切片处理代码,则无需一遍又一遍地复制它。

处理 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)]

这看起来要简单得多,但我在这里通过将第二个索引转发到 normal 来作弊list,这只是因为我的底层存储是 a listof lists。但是,如果您有任何类型的可索引行对象要遵循(并且它不会浪费不可接受的时间/空间来不必要地创建这些对象),您可以使用相同的作弊方法。


如果您反对在 index 参数上进行类型切换,是的,这似乎通常是 unpythonic,但不幸的是它__getitem__通常是如何工作的。如果你想使用通常的 EAFTPtry逻辑,你可以,但是当你必须在多个地方尝试两个不同的 API(例如,[0]用于元组和.start切片)时,我认为它不会更具可读性。您最终会在顶部进行“鸭子类型切换”,如下所示:

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

……等等,这只是没有任何通常好处的普通类型切换的代码的两倍。

于 2013-03-27T20:53:53.063 回答