在Python中实现2D切片
我正在用Python实现一个线性代数库(我知道可能已经有现成的库,但我这样做是为了学习Python和我考试需要的数学知识),我想能够像这样访问矩阵的元素或子集:
(我的矩阵类是元组的一个子类。)
M = Matrix([元素的行列表])
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:
块,但不确定怎么把它们串联起来,而且我也不想嵌套太多。
所以,非常感谢你的阅读。实现这样的功能最好的方法是什么呢?
1 个回答
你基本上需要做一些类似这样的事情……不过至少你可以减少一些重复的代码。
首先,考虑 [1,]
表示“第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
的 list
。不过如果你有任何可以索引的行对象可以使用(并且创建这些对象不会浪费过多的时间或空间),你也可以使用同样的技巧。
如果你反对在索引参数上进行类型转换,是的,这确实看起来不太符合 Python 的风格,但不幸的是,这就是 __getitem__
通常的工作方式。如果你想使用常见的 EAFTP try
逻辑,可以这样做,但我觉得在多个地方尝试两种不同的 API(例如,[0]
用于元组,.start
用于切片)并不会让代码更易读。你最终会在顶部做“鸭子类型切换”,像这样:
try:
idx[0]
except AttributeError:
idx = (idx, slice(None, None, None))
……如此等等,这样的代码量是正常类型切换的两倍,没有任何通常的好处。