如何在初始化 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)
我天真地假设通过省略号[...]
赋值,例如
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".
关于从 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)