Numpy __getitem__ 延迟计算与 a[-1:] 和 a[slice(-1, None, None)] 不相同
我有两个问题,感觉是同一个基本概念搞混了。希望这样没问题。
这里有段代码:
import numpy as np
class new_array(np.ndarray):
def __new__(cls, array, foo):
obj = array.view(cls)
obj.foo = foo
return obj
def __array_finalize__(self, obj):
print "__array_finalize"
if obj is None: return
self.foo = getattr(obj, 'foo', None)
def __getitem__(self, key):
print "__getitem__"
print "key is %s"%repr(key)
print "self.foo is %d, self.view(np.ndarray) is %s"%(
self.foo,
repr(self.view(np.ndarray))
)
self.foo += 1
return super(new_array, self).__getitem__(key)
print "Block 1"
print "Object construction calls"
base_array = np.arange(20).reshape(4,5)
print "base_array is %s"%repr(base_array)
p = new_array(base_array, 0)
print "\n\n"
print "Block 2"
print "Call sequence for p[-1:] is:"
p[-1:]
print "p[-1].foo is %d\n\n"%p.foo
print "Block 3"
print "Call sequence for s = p[-1:] is:"
s = p[-1:]
print "p[-1].foo is now %d"%p.foo
print "s.foo is now %d"%s.foo
print "s.foo + p.foo = %d\n\n"%(s.foo + p.foo)
print "Block 4"
print "Doing q = s + s"
q = s + s
print "q.foo = %d\n\n"%q.foo
print "Block 5"
print "Printing s"
print repr(s)
print "p.foo is now %d"%p.foo
print "s.foo is now %d\n\n"%s.foo
print "Block 6"
print "Printing q"
print repr(q)
print "p.foo is now %d"%p.foo
print "s.foo is now %d"%s.foo
print "q.foo is now %d\n\n"%q.foo
print "Block 7"
print "Call sequence for p[-1]"
a = p[-1]
print "p[-1].foo is %d\n\n"%a.foo
print "Block 8"
print "Call sequence for p[slice(-1, None, None)] is:"
a = p[slice(-1, None, None)]
print "p[slice(None, -1, None)].foo is %d"%a.foo
print "p.foo is %d"%p.foo
print "s.foo + p.foo = %d\n\n"%(s.foo + p.foo)
这段代码的输出是:
Block 1
Object construction calls
base_array is array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
__array_finalize
Block 2
Call sequence for p[-1:] is:
__array_finalize
p[-1].foo is 0
Block 3
Call sequence for s = p[-1:] is:
__array_finalize
p[-1].foo is now 0
s.foo is now 0
s.foo + p.foo = 0
Block 4
Doing q = s + s
__array_finalize
q.foo = 0
Block 5
Printing s
__getitem__
key is -1
self.foo is 0, self.view(np.ndarray) is array([[15, 16, 17, 18, 19]])
__array_finalize
__getitem__
key is -5
self.foo is 1, self.view(np.ndarray) is array([15, 16, 17, 18, 19])
__getitem__
key is -4
self.foo is 2, self.view(np.ndarray) is array([15, 16, 17, 18, 19])
__getitem__
key is -3
self.foo is 3, self.view(np.ndarray) is array([15, 16, 17, 18, 19])
__getitem__
key is -2
self.foo is 4, self.view(np.ndarray) is array([15, 16, 17, 18, 19])
__getitem__
key is -1
self.foo is 5, self.view(np.ndarray) is array([15, 16, 17, 18, 19])
new_array([[15, 16, 17, 18, 19]])
p.foo is now 0
s.foo is now 1
Block 6
Printing q
__getitem__
key is -1
self.foo is 0, self.view(np.ndarray) is array([[30, 32, 34, 36, 38]])
__array_finalize
__getitem__
key is -5
self.foo is 1, self.view(np.ndarray) is array([30, 32, 34, 36, 38])
__getitem__
key is -4
self.foo is 2, self.view(np.ndarray) is array([30, 32, 34, 36, 38])
__getitem__
key is -3
self.foo is 3, self.view(np.ndarray) is array([30, 32, 34, 36, 38])
__getitem__
key is -2
self.foo is 4, self.view(np.ndarray) is array([30, 32, 34, 36, 38])
__getitem__
key is -1
self.foo is 5, self.view(np.ndarray) is array([30, 32, 34, 36, 38])
new_array([[30, 32, 34, 36, 38]])
p.foo is now 0
s.foo is now 1
q.foo is now 1
Block 7
Call sequence for p[-1]
__getitem__
key is -1
self.foo is 0, self.view(np.ndarray) is array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
__array_finalize
p[-1].foo is 1
Block 8
Call sequence for p[slice(-1, None, None)] is:
__getitem__
key is slice(-1, None, None)
self.foo is 1, self.view(np.ndarray) is array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
__array_finalize
p[slice(None, -1, None)].foo is 2
p.foo is 2
s.foo + p.foo = 3
请注意两点:
调用
p[-1:]
并不会触发new_array.__getitem__
。如果把p[-1:]
换成p[0:]
、p[0:-1]
等等,情况也是一样。但像p[-1]
和p[slice(-1, None, None)]
这样的调用会触发new_array.__getitem__
。对于像p[-1:] + p[-1:]
或s = p[-1]
这样的语句也是如此,但print s
就不会触发。你可以通过上面提到的“块”来观察这一点。在调用
new_array.__getitem__
时,变量foo
会被正确更新(见块 5 和 6),但在new_array.__getitem__
执行完后,foo
的值就不对了(再次见块 5 和 6)。我还要补充一点,把return super(new_array, self).__getitem__(key)
这一行替换成return new_array(np.array(self.view(np.ndarray)[key]), self.foo)
也不行。以下的块是输出的唯一不同之处。Block 5 Printing s __getitem__ key is -1 self.foo is 0, self.view(np.ndarray) is array([[15, 16, 17, 18, 19]]) __array_finalize__ __getitem__ key is -5 self.foo is 1, self.view(np.ndarray) is array([15, 16, 17, 18, 19]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -4 self.foo is 2, self.view(np.ndarray) is array([15, 16, 17, 18, 19]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -3 self.foo is 3, self.view(np.ndarray) is array([15, 16, 17, 18, 19]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -2 self.foo is 4, self.view(np.ndarray) is array([15, 16, 17, 18, 19]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -1 self.foo is 5, self.view(np.ndarray) is array([15, 16, 17, 18, 19]) __array_finalize__ __array_finalize__ __array_finalize__ new_array([[15, 16, 17, 18, 19]]) p.foo is now 0 s.foo is now 1 Block 6 Printing q __getitem__ key is -1 self.foo is 0, self.view(np.ndarray) is array([[30, 32, 34, 36, 38]]) __array_finalize__ __getitem__ key is -5 self.foo is 1, self.view(np.ndarray) is array([30, 32, 34, 36, 38]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -4 self.foo is 2, self.view(np.ndarray) is array([30, 32, 34, 36, 38]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -3 self.foo is 3, self.view(np.ndarray) is array([30, 32, 34, 36, 38]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -2 self.foo is 4, self.view(np.ndarray) is array([30, 32, 34, 36, 38]) __array_finalize__ __array_finalize__ __array_finalize__ __getitem__ key is -1 self.foo is 5, self.view(np.ndarray) is array([30, 32, 34, 36, 38]) __array_finalize__ __array_finalize__ __array_finalize__ new_array([[30, 32, 34, 36, 38]]) p.foo is now 0 s.foo is now 1 q.foo is now 1
这现在包含了过多的
new_array.__array_finalize__
调用,但foo
的“问题”没有改变。我原本以为,像
p[-1:]
这样的调用会让p.foo = 0
后,p.foo == 1
返回True
。显然不是这样,即使在调用__getitem__
时foo
被正确更新,因为像p[-1:]
这样的调用会导致大量的__getitem__
调用(考虑到延迟计算)。而且p[-1:]
和p[slice(-1, None, None)]
的foo
值会不同(如果计数正常的话)。在前者中,foo
会加上5
,而在后者中,foo
会加上1
。
问题
虽然 numpy 数组切片的延迟计算不会在我的代码执行时造成问题,但在用 pdb 调试我的一些代码时却非常麻烦。基本上,语句在运行时和在 pdb 中的表现似乎不同。我觉得这不好。这就是我发现这种行为的原因。
我的代码使用传给 __getitem__
的输入来判断应该返回什么类型的对象。在某些情况下,它返回同一类型的新实例,在其他情况下,它返回其他类型的新实例,或者返回一个 numpy 数组、标量或浮点数(具体取决于底层 numpy 数组认为正确的是什么)。我使用传给 __getitem__
的键来确定返回哪个正确的对象。但如果用户传入的是切片,比如 p[-1:]
,我就无法做到这一点,因为这个方法只会得到单个索引,就像用户写 p[4]
一样。那么,如果我 numpy 子类的 __getitem__
中的 key
无法反映用户是请求切片(如 p[-1:]
),还是仅仅是某个元素(如 p[4]
),我该怎么做呢?
顺便提一下,numpy 索引 的文档暗示切片对象,比如 slice(start, stop, step)
会和像 start:stop:step
这样的语句被视为相同。这让我觉得我可能漏掉了一些非常基本的东西。这个暗示出现在很早的地方:
基本切片发生在对象是切片对象(通过在括号内使用 start:stop:step 语法构造)时,或者是整数,或者是切片对象和整数的元组。
我不禁觉得,这个基本错误也是我认为 self.foo += 1
这一行应该计算用户请求切片或实例元素的次数(而不是切片中元素的数量)的原因。这两个问题实际上是相关的吗?如果是的话,怎么相关呢?
2 个回答
使用 isinstance
方法来检查一个对象是否是切片类型。
from __future__ import print_function
class SliceExample(object):
def __getitem__(self, key):
if isinstance(key, slice):
return key.start, key.stop
return key
sl = SliceExample()
print(repr(sl[1]))
print(repr(sl[1:2]))
你确实遇到了一个麻烦的错误。知道我不是唯一一个遇到这个问题的人,心里稍微轻松了一点!幸运的是,这个问题很容易解决。只需要在你的类里面加上类似下面的代码。这实际上是我几个月前写的一段代码的复制粘贴,文档字符串大致说明了发生了什么,但你可能还想看看Python的文档。
def __getslice__(self, start, stop) :
"""This solves a subtle bug, where __getitem__ is not called, and all
the dimensional checking not done, when a slice of only the first
dimension is taken, e.g. a[1:3]. From the Python docs:
Deprecated since version 2.0: Support slice objects as parameters
to the __getitem__() method. (However, built-in types in CPython
currently still implement __getslice__(). Therefore, you have to
override it in derived classes when implementing slicing.)
"""
return self.__getitem__(slice(start, stop))