如何在初始化 numpy 对象数组时避免额外的浮动对象副本

How to avoid additional float-object-copies while initializing a numpy object-array

我天真地假设通过省略号[...]赋值,例如

a = np.empty(N, dtype=np.object)
a[...] = 0.0

基本上是以下朴素循环的更快版本:

def slow_assign_1d(a, value):
    for i in range(len(a)):
        a[i] = value

然而,情况似乎并非如此。这是不同行为的示例:

>>> a=np.empty(2, dtype=np.object)
>>> a[...] = 0.0
>>> a[0] is a[1]
False

对象 0.0 似乎被克隆了。然而,当我使用天真的慢速版本时:

>>> a=np.empty(2, dtype=np.object)
>>> slow_assign(a, 0.0)
>>> a[0] is a[1]
True

所有元素都是"same".

有趣的是,可以观察到带有省略号的所需行为,例如使用自定义 class:

>>> class A:
       pass
>>> a[...]=A()
>>> a[0] is a[1]
True 

为什么 get floats 这种 "special" 处理,有没有一种方法可以使用 float 值快速初始化而不产生副本?

注意:np.full(...)a[:] 显示与 a[...] 相同的行为:对象 0.0 是 cloned/its 创建了副本。


编辑: 正如@Till Hoffmann 指出的那样,字符串和整数的预期行为仅适用于小整数 (-5...255) 和短字符串 (一个字符),因为它们来自一个池,并且此类对象不会超过一个。

>>> a[...] = 1         # or 'a'
>>> a[0] is a[1]
True
>>> a[...] = 1000      # or 'aa'
>>> a[0] is a[1]
False

似乎 "desired behavior" 仅适用于 numpy 无法向下转型的类型,例如:

>>> class A(float): # can be downcasted to a float
>>>     pass
>>> a[...]=A()
>>> a[0] is a[1]
False

更重要的是,a[0]不再是A类型,而是float.

类型

这实际上是整数而不是浮点数的问题。特别是, "small" 整数缓存在 python 中,这样它们都指向同一内存,因此具有相同的 id,因此与 is 运算符相比是相同的.对于花车来说,情况并非如此。 "small".

的官方定义见"is" operator behaves unexpectedly with integers for a more in-depth discussion. See https://docs.python.org/3/c-api/long.html#c.PyLong_FromLong

关于从 float 继承的 A 的特定示例,numpy documentation 指出

Note that assignments may result in changes if assigning higher types to lower types [...]

有人可能会争辩说,在上面提供的示例中,没有将较高类型分配给较低类型,因为 np.object 应该是最通用的类​​型。但是,检查数组元素的类型后,很明显在使用 [...] 赋值时,类型被向下转换为 float

a = np.empty(2, np.object)

class A(float):
    pass

a[0] = a[1] = A()
print(type(a[0]))  # <class '__main__.A'>
a[...] = A()
print(type(a[0]))  # <class 'float'>

顺便说一句:除非单个对象非常大,否则您可能无法通过存储对感兴趣对象的引用来节省大量内存。例如。存储单精度浮点数比存储指向它的指针(在 64 位系统上)便宜。如果您的对象确实非常大,它们(可能)不能向下转换为原始类型,因此问题不太可能首先出现。

此行为是一个 numpy 错误:https://github.com/numpy/numpy/issues/11701

因此,在错误修复之前,可能必须使用一种变通方法。我最终使用了带有 cython 的天真慢速版本 implemented/compiled,例如这里的一维和 np.full:

%%cython
cimport numpy as np
import numpy as np
def cy_full(Py_ssize_t n, object obj):
    cdef np.ndarray[dtype=object] res = np.empty(n, dtype=object)
    cdef Py_ssize_t i
    for i in range(n):
        res[i]=obj
    return res

a=cy_full(5, np.nan)

a[0] is a[4]  # True as expected!

np.full相比也没有性能劣势:

%timeit cy_full(1000, np.nan)
# 8.22 µs ± 39.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit np.full(1000, np.nan, dtype=np.object)
# 22.3 µs ± 129 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)