NumPy:为什么需要显式复制一个值?

8
arr = np.arange(0,11)
slice_of_arr = arr[0:6]
slice_of_arr[:]=99

# slice_of_arr returns
array([99, 99, 99, 99, 99, 99])
# arr returns
array([99, 99, 99, 99, 99, 99,  6,  7,  8,  9, 10])

如上所示的例子,你不能直接更改 slice_of_arr 的值,因为它是 arr 的视图,而不是一个新变量。

我的问题是:

  1. 为什么 NumPy 要这样设计?每次都需要使用 .copy 并赋值,这不会很繁琐吗?
  2. 有什么方法可以摆脱 .copy 吗?我该如何改变 NumPy 的默认行为?

10
这样设计是为了提高性能,而实现如写时复制(copy-on-write)等可能会更加令人困惑。您为何认为这种设计有问题? - Krumelur
@Krumelur 因为在大多数非科学编程场景中,这就是它的工作方式。我认为在 matlab 中也没有相应的等价物。我必须要“记住”这种行为,而我真的不喜欢编程中的“重复”。 - ZK Zhao
6
答案就像@Krumelur所说的那样:NumPy被优化得执行效率很高。不必要的复制需要时间,更重要的是还会增加数据大小。增加的数据大小导致缓存命中率降低,在现代CPU上优化吞吐量时,缓存命中率是最重要的部分。如果你需要一份副本,请明确地复制,但最好知道你已经这样做了,而不是在添加新变量后突然发现性能急剧下降。 - Borealid
6
如果你更看重代码的清晰度而不是速度,我建议你不要使用NumPy。NumPy是为大规模高性能的科学计算而设计的。 - Borealid
2
@cqcn1991 更进一步解释:仅仅添加一个开关来改变行为本身就是性能开销。每次进行数组赋值时检查该开关的位置都会消耗运行时性能。这个成本由那些谨慎编写NumPy代码的人承担。每个项目都选择他们要优化的目标;NumPy优化无与伦比的运行时性能,同时仍然使用Python语言。 - Borealid
显示剩余3条评论
2个回答

3
我认为其他评论中已经回答了你的问题,但更具体地说:
1.a. 为什么NumPy要设计成这样?因为创建视图比创建整个数组速度更快(常数时间)。
1.b. 每次需要.copy并赋值不会很繁琐吗?实际上,需要创建副本并不是很常见。所以不,它并不繁琐。即使这种设计一开始可能会让人感到惊讶,但非常好。
2.a. 有没有什么方法可以摆脱.copy?我无法在看到真实代码之前告诉你。在你给出的玩具示例中,你无法避免创建副本,但在真实代码中,通常将函数应用于数据,该函数返回另一个数组,因此不需要副本。你能给出一个需要反复调用.copy的真实代码示例吗?
2.b. 如何更改NumPy的默认行为?你不能。尝试习惯它,你会发现它有多么强大。

1

(numpy) __array_wrap__是做什么的?

讨论了关于ndarray子类和钩子(如__array_wrap__)的话题。np.array接受copy参数,即使其他方面不需要,也会强制结果为副本。 ravel返回一个视图,而flatten则返回一个副本。因此,可能可以构造一个ndarray子类来强制复制,这可能涉及修改像__array_wrap__这样的钩子。

或者可能修改.__getitem__方法。像slice_of_arr = arr[0:6]这样的索引涉及调用__getitem__。对于ndarray,这是编译的,但对于掩码数组,它是您可以用作示例的Python代码:

/usr/lib/python3/dist-packages/numpy/ma/core.py

这可能只是一些简单的东西。
def __getitem__(self, indx):
    """x.__getitem__(y) <==> x[y]
    """
    # _data = ndarray.view(self, ndarray) # change to:
    _data = ndarray.copy(self, ndarray)
    dout = ndarray.__getitem__(_data, indx)
    return dout

我猜当你开发并完全测试这样的子类时,你可能会爱上默认的无复制方法。虽然这种视图与复制业务会影响许多新手(特别是从MATLAB转来的),但我没有看到有经验的用户抱怨。看看其他numpy SO问题;你不会看到很多copy()调用。
即使是常规的Python用户也习惯于问自己一个引用或切片是否为副本,以及某些东西是否可变。
例如,对于列表:
In [754]: ll=[1,2,[3,4,5],6]
In [755]: llslice=ll[1:-1]
In [756]: llslice[1][1:2]=[10,11,12]
In [757]: ll
Out[757]: [1, 2, [3, 10, 11, 12, 5], 6]

修改一个切片内部的项目将会修改原始列表中相应的项。与numpy相比,列表切片是副本,但是这是浅拷贝。您需要额外努力进行深拷贝(import copy)。 /usr/lib/python3/dist-packages/numpy/lib/index_tricks.py包含一些旨在使某些索引操作更方便的索引函数。有几个实际上是类或类实例,具有自定义的__getitem__方法。它们还可以作为如何自定义切片和索引的模型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接