在Python中实现2D切片

4
我正在使用Python实现一个线性代数库(我知道可能已经有类似的库,但我想通过这个项目学习Python和我考试需要的数学知识),我希望能够像下面这样访问矩阵的元素/子集:
(我的矩阵类是tuple的子类。)
- `M = Matrix([元素行列表])` - `M[1, 2]` 获取(1, 2)处的元素 - `M[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:块,但我不知道如何链接它们,而且我不想嵌套太多。
所以,非常感谢阅读。最佳的实现此类函数的方法是什么?

PS,一旦你自己构建完成,请查看numpy,它是最突出的已经存在的东西。它可以做你想要的一切甚至更多,并且很可能会使你的代码更易于阅读并且更快。我不想立即提到这一点,因为我不想阻止你享受自己设计的乐趣...但我也希望确保你不会错过玩numpy的乐趣。 - abarnert
不,我知道 numpy。虽然我只写了大约三天的 Python,还没有使用它。话说我一直在写 Python。我正在攻读数学学位,所以我更倾向于这方面。再次感谢! - Max Bucknell
1个回答

4

你基本上需要像这样做...但至少可以减少一些重复。

首先,认为[1,][1]是同一个意思可能是合理的。(numpy这样做。)这意味着您不需要元组与整数之间的差异;只需将整数视为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

其次,尽管您的示例代码只处理了两个切片的情况,但您的真实代码必须处理两个切片,或一个切片和一个整数,或一个整数和一个切片,或两个整数,或一个切片,或一个整数。如果您可以将切片处理代码分解出来,就不需要一遍又一遍地重复它。
处理整数与切片之间的技巧是将 `[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的列表。但是,如果你有任何可索引的行对象可以使用(并且不会浪费不必要的时间/空间来创建这些对象),您可以使用同样的 cheat。


如果您反对需要在索引参数上进行类型切换,那么是的,这似乎一般上不符合Python语言的理念,但不幸的是它是如何__getitem__通常工作的。如果你想使用通常的EAFTP try逻辑,你当然可以,但是当您不得不在多个地方尝试两种不同的API时(例如tuples使用[0],slices使用.start),我认为这样做并没有更易读。您最终在顶部执行“鸭式类型切换”,如下所示:

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

...等等,这比普通的类型切换多了两倍的代码量,而且没有任何常规的好处。


太好了,我的答案和你的第二个答案非常接近;非常感谢。由于我从元组继承,所以无需担心 setitem 或内部存储方法。(我还编写了一个向量类,我的矩阵是向量的装饰元组,而向量本身也是装饰元组。) - Max Bucknell
1
使用一个1D(N*M长度)元组作为内部存储绝对是合理的,但您可能希望考虑封装和委托而不是继承。 - abarnert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接