我正在用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:
块,但我不确定如何将它们链接起来,而且我真的不想嵌套太多。
所以,所以,谢谢你的阅读。实现这样的函数的最佳方法是什么?
答案 0 :(得分:3)
你几乎必须这样做某事 ...但至少你可以删除一些重复。
首先,将[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-vs.-slice的一个技巧是将[n]
视为一个包装器,实质上是[n:n+1][0]
,它可以让你更多地减少所有内容。 (这有点比这更棘手,因为你必须特殊情况下一般是负数,或只是-1
,因为显然n[-1] != n[-1:0][0]
。)对于一维数组,这可能不值得,但对于2D数组,它可能是,因为这意味着当你处理列时,你总是得到一个行列表而不是一行。
另一方面,您可能希望在__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
秒。但是如果您有任何类型的可索引行对象(并且不会浪费不必要的时间/空间来不必要地创建这些对象),则可以使用相同的作弊。
如果你反对在index参数上键入开关的需要,是的,这似乎通常是unpythonic,但不幸的是__getitem__
通常是如何工作的。如果你想使用通常的EAFTP try
逻辑,你可以,但我不认为当你必须尝试两个不同的API(例如,[0]
用于元组和{{1}时,它更具可读性多个地方的切片)你最终会在顶部进行“鸭型切换”,如下所示:
.start
......等等,这只是正常类型切换的两倍,没有任何常见的好处。