转移numpy数据的所有权

10 投票
3 回答
2723 浏览
提问于 2025-04-17 19:10

在我之前的一个问题中,我学会了如何在原地调整一个子类的 ndarray 的大小。这很不错。不过,当我尝试调整一个计算结果得到的数组大小时,这个方法就不管用了。

import numpy as np

class Foo(np.ndarray):
    def __new__(cls,shape,dtype=np.float32,buffer=None,offset=0,
                strides=None,order=None):
        return np.ndarray.__new__(cls,shape,dtype,buffer,offset,strides,order)

    def __array_prepare__(self,output,context):
        print output.flags['OWNDATA'],"PREPARE",type(output)
        return np.ndarray.__array_prepare__(self,output,context)

    def __array_wrap__(self,output,context=None):
        print output.flags['OWNDATA'],"WRAP",type(output)

        return np.ndarray.__array_wrap__(self,output,context)

a = Foo((32,))
#resizing a is no problem
a.resize((24,),refcheck=False)

b = Foo((32,))
c = Foo((32,))

d = b+c
#Cannot resize `d`
d.resize((24,),refcheck=False)

具体的输出结果(包括错误追踪信息)是:

True PREPARE <type 'numpy.ndarray'>
False WRAP <class '__main__.Foo'>
Traceback (most recent call last):
  File "test.py", line 26, in <module>
    d.resize((24,),refcheck=False)
ValueError: cannot resize this array: it does not own its data

我觉得这是因为 numpy 创建了一个新的 ndarray,然后把它传给了 __array_prepare__。但在这个过程中,似乎这个“output”数组被“视图转换”成了我的 Foo 类型,尽管文档在这一点上并不是特别清楚或准确。无论如何,在视图转换之后,输出数组不再拥有数据,这使得我无法在原地调整它的形状(就我所知是这样)。

有没有什么办法,通过某种 numpy 的技巧(比如 __array_prepare____array__ 等等)把数据的所有权转移给我的子类实例呢?

3 个回答

0

这样怎么样:

def resize(arr, shape):
    np.require(arr, requirements=['OWNDATA'])
    arr.resize(shape, refcheck=False)

看起来这个方法在调整大小(并减少内存使用)方面是成功的:

import array
import numpy as np
import time

class Foo(np.ndarray):
    def __new__(cls, shape, dtype=np.float32, buffer=None, offset=0,
                strides=None, order=None):
        return np.ndarray.__new__(cls, shape, dtype, buffer, offset, strides, order)

    def __array_prepare__(self, output, context):
        print(output.flags['OWNDATA'], "PREPARE", type(output))
        return np.ndarray.__array_prepare__(self, output, context)

    def __array_wrap__(self, output, context=None):
        print(output.flags['OWNDATA'], "WRAP", type(output))
        output = np.ndarray.__array_wrap__(self, output, context)
        return output

def free_memory():
    """
    Return free memory available, including buffer and cached memory
    """
    total = 0
    with open('/proc/meminfo', 'r') as f:
        for line in f:
            line = line.strip()
            if any(line.startswith(field) for field in ('MemFree', 'Buffers', 'Cached')):
                field, amount, unit = line.split()
                amount = int(amount)
                if unit != 'kB':
                    raise ValueError(
                        'Unknown unit {u!r} in /proc/meminfo'.format(u=unit))
                total += amount
    return total


def gen_change_in_memory():
    """
    http://stackoverflow.com/a/14446011/190597 (unutbu)
    """
    f = free_memory()
    diff = 0
    while True:
        yield diff
        f2 = free_memory()
        diff = f - f2
        f = f2
change_in_memory = gen_change_in_memory().next

def resize(arr, shape):
    print(change_in_memory())
    # 0
    np.require(arr, requirements=['OWNDATA'])

    time.sleep(1)
    print(change_in_memory())
    # 200

    arr.resize(shape, refcheck=False)

N = 10000000
b = Foo((N,), buffer = array.array('f',range(N)))
c = Foo((N,), buffer = array.array('f',range(N)))

结果是

print(change_in_memory())
# 0

d = b+c
d = np.require(d, requirements=['OWNDATA'])

print(change_in_memory())
# 39136

resize(d, (24,))   # Increases memory by 200 KiB
time.sleep(1)
print(change_in_memory())
# -39116
1

在某个时候,似乎“输出”数组被转换成了我的 Foo 类型。

没错,ndarray.__array_prepare__ 会调用 output.view,这个方法返回一个不拥有自己数据的数组。

我试了一些方法,没找到简单的解决办法。

虽然我同意这种行为并不是最理想的,但在你的使用场景中,我认为 d 不拥有自己的数据也是可以接受的。Numpy 广泛使用视图,如果你坚持在使用 Numpy 数组时避免创建任何视图,那你会让自己变得很麻烦。

根据我的经验,我还认为一般情况下应该避免使用 resize。如果你不使用 resize,处理创建的视图应该不会有问题。使用 resize 有点像是在走捷径,而且很难处理(你可能已经开始理解,使用它时会遇到两个经典错误之一:它不拥有自己的数据。另一个是 不能调整已被引用的数组的大小)。还有一个问题在这个问题中描述过。

由于你决定使用 resize 是基于你另一个问题的回答,我会把我的剩余回答放在那里

6

这可能不是一个很满意的答案,但也不适合放在评论里……你可以通过使用ufunc的out参数来解决数据拥有权的问题。这里有个简单的例子:

>>> a = Foo((5,))
>>> b = Foo((5,))
>>> c = a + b # BAD
True PREPARE <type 'numpy.ndarray'>
False WRAP <class '__main__.Foo'>
>>> c.flags.owndata
False

>>> c = Foo((5,))
>>> c[:] = a + b # BETTER
True PREPARE <type 'numpy.ndarray'>
False WRAP <class '__main__.Foo'>
>>> c.flags.owndata
True

>>> np.add(a, b, out=c) # BEST
True PREPARE <class '__main__.Foo'>
True WRAP <class '__main__.Foo'>
Foo([  1.37754085e-38,   1.68450356e-20,   6.91042737e-37,
         1.74735556e-04,   1.48018885e+29], dtype=float32)
>>> c.flags.owndata
True

我认为上面的输出和c[:] = a + b是一样的,都是让c拥有数据,但这需要把数据从一个临时数组复制过来。不过,当你使用out参数时,这种情况就不应该发生。

既然你已经担心你的数学表达式中会有临时存储,那么微管理这些存储的方式也未必是坏事。也就是说,把

g = a + b + np.sqrt(d*d + e*e + f*f)

替换成

g = foo_like(d) # you'll need to write this function!
np.multiply(d, d, out=g)
g += e * e
g += f * f
np.sqrt(g, out=g)
g += b
g += a

可能会节省一些临时内存,并且让你拥有自己的数据。虽然这样做可能会让代码的“可读性”下降,但……

撰写回答