【问题标题】:Numpy array indexing: view or copy - depends on scope?Numpy 数组索引:查看或复制 - 取决于范围?
【发布时间】:2022-01-23 05:38:43
【问题描述】:

考虑以下数组操作:

import numpy as np
def f(x):
     x += 1
x = np.zeros(1)
f(x)       # changes `x`
f(x[0])    # doesn't change `x`
x[0] += 1  # changes `x`

为什么x[0] 的行为会根据+= 1 发生在函数f 内部还是外部而有所不同?

我可以将数组的一部分传递给函数,以便函数修改原始数组吗?


编辑:如果我们考虑= 而不是+=,我们可能会保持问题的核心,同时消除一些不相关的复杂性。

【问题讨论】:

  • 因为你传递 x[0] ,你只是传递那个值(作为一个对象)而不是整个 x 对象
  • @eshirvana 如果passing复制,为什么f(x)修改x?如果 indexing 进行复制,为什么x[0] += 1 会修改原件?如果passing只有在有indexing的情况下才会做一个copy,那么passing怎么知道有没有indexing,为什么要实现这个依赖呢?
  • x 是可变的,x[0] 不是。这意味着__iadd__ 可以修改x 但不能修改x[0]。这实际上与索引无关。如果x[0] 是可变的并且定义了__iadd__ 方法来修改它,那也会改变。用二维数组试试,你会看到传递x[0]会改变它的第一行。
  • 是的,__iadd__+=。当您执行x[0] = 1x[0] = x[0] + 1x[0] += 1 并且x[0] 是一个整数时,您并没有真正修改那里的整数,而是将其指向不同的整数。 Ned Batchelder 对此有很好的article
  • @root 是的,就是这样。 __getitem__ 返回一个新对象,__setitem__ 改变底层缓冲区...您必须了解这些基本上只是调用方法的糖。原则上,它们可以做任何事情,而且它们不必彼此保持一致

标签: python numpy scope pass-by-reference numpy-ndarray


【解决方案1】:

您甚至不需要函数调用就能看到这种差异。

x 是一个数组:

In [138]: type(x)
Out[138]: numpy.ndarray

索引数组元素会返回一个np.float64 对象。它实际上从数组中“取出”值;它不是对数组元素的引用。

In [140]: y=x[0]
In [141]: type(y)
Out[141]: numpy.float64

这个y 很像python float;你可以+=同样的方式:

In [142]: y += 1
In [143]: y
Out[143]: 1.0

但这不会改变x:

In [144]: x
Out[144]: array([0.])

但这确实改变了x

In [145]: x[0] += 1
In [146]: x
Out[146]: array([1.])

y=x[0] 调用x.__getitem__x[0]=3 拨打x.__setitem__ 电话。 += 使用 __iadd__,但效果相似。

另一个例子:

更改x:

In [149]: x[0] = 3
In [150]: x
Out[150]: array([3.])

但尝试对y 执行相同操作失败:

In [151]: y[()] = 3
Traceback (most recent call last):
  File "<ipython-input-151-153d89268cbc>", line 1, in <module>
    y[()] = 3
TypeError: 'numpy.float64' object does not support item assignment

y[()] 是允许的。

basic 对带有切片的数组进行索引确实会生成一个可以修改的 view

In [154]: x = np.zeros(5)
In [155]: x
Out[155]: array([0., 0., 0., 0., 0.])
In [156]: y= x[0:2]
In [157]: type(y)
Out[157]: numpy.ndarray
In [158]: y += 1
In [159]: y
Out[159]: array([1., 1.])
In [160]: x
Out[160]: array([1., 1., 0., 0., 0.])

===

x[0]+=1 类操作的 Python 列表和字典示例:

In [405]: alist = [1,2,3]
In [406]: alist[1]+=12
In [407]: alist
Out[407]: [1, 14, 3]
In [408]: adict = {'a':32}
In [409]: adict['a'] += 12
In [410]: adict
Out[410]: {'a': 44}

__iadd__ 可以认为是 __getitem__ 后跟具有相同索引的 __setitem__

【讨论】:

  • 为什么x[0] += 1 会更改数组,即使它仅通过索引访问数组x,并且您编写索引“将值从数组中取出;它不是引用数组的元素”?可能是因为有两种不同的索引__getitem____setitem__(后者是指索引发生在等号左侧,可以这么说),根据this answer
  • += 操作并非数组独有。我添加了带有列表和字典的示例。
【解决方案2】:

问题不在于范围,因为唯一取决于范围的是可用名称。所有对象都可以在任何为其命名的范围内访问。问题在于可变性与不变性以及理解运算符的作用。

x 是一个可变的 numpy 数组。 f 直接在其上运行 x += 1+= 是调用就地添加的运算符。换句话说,它确实是x = x.__iadd__(1)*。请注意对x 的重新分配,这发生在函数中。这是就地运算符的一个特性,允许它们对不可变对象进行操作。在这种情况下,ndarray.__iadd__ 是一个真正的就地运算符,它只返回 x,一切都按预期工作。

现在让我们以同样的方式分析f(x[0])x[0] 调用 x.__getitem__(0)*。当你传入一个标量 int 索引时,numpy 会提取一个单元素数组并在其上有效地调用 .item()。结果是 python int(或float,甚至可能是tuple,取决于您的数组的dtype 是什么)。无论哪种方式,对象都是不可变的。一旦它被__getitem__ 提取,f 中的+= 运算符将名称x in f 替换为新对象,但在函数之外看不到更改,数组中少得多。在这种情况下,f 没有引用 x,因此不会发生任何变化。

x[0] += 1 的示例与调用f(x[0]) 不同。相当于调用x.__setitem__(0, x.__getitem__(0).__iadd__(1))*。对f 的调用只是type(x).__getitem__(0).__iadd__(1) 的一部分,它返回一个新对象,但不会像__setitem__ 那样重新分配。 关键是python中的[] =__setitem__)是一个完全不同于[]__getitem__)和=(assingment)的运算符

要使第二个示例 (f(x[0]) 正常工作,您必须传入一个可变对象。整数对象提取单个 python 对象,数组索引制作副本。但是,slice 索引会返回一个可变视图并绑定到原始数​​组内存。因此,您可以这样做

f(x[0:1])  # changes `x`

在这种情况下,f 执行以下操作:x.__getitem__(slice(0, 1, None)).__iadd__(1)。关键是__getitem__ 将可变视图返回到原始数组中,而不是不可变的int

要了解为什么不仅对象是可变的而且它是原始数组的视图很重要,请尝试f(x[[0]])。使用列表进行索引会产生一个数组,但会产生一个副本。在x[[0]].__iadd__ 中会修改你传入的列表,但不会将列表复制回原始列表,因此更改不会传播。


* 这是一个近似值。当被操作员调用时,dunder 方法实际上被称为type(x).__operator__(x, ...),而不是x.__operator__(...)

【讨论】:

  • 你写“关键是 python 中的[] = (__setitem__) 是与[] (__getitem__) 和= (assingment) 完全不同的运算符。”你还记得一个很好的解释吗,例如在 Python 文档中?
  • 这句话很重要,请在答案中加粗:“关键是python中的[] =(__setitem__)与[](__getitem__)是完全不同的运算符和=(分配)分开。” (我无法建议编辑,因为建议编辑队列已满。)谢谢! :)
  • @root。这只是方法的定义和我个人的解释。当我第一次学习时,这个见解对我很有帮助,所以我很高兴为你加粗。
【解决方案3】:

根据this commentthis answer

  • f(x[0]) 内部的x[0]x 上执行__getitem__。在这种特殊情况下(例如,与索引数组的 slice 不同),此操作返回的值不允许修改原始数组。
  • x[0] = 1x 上执行 __setitem__

__getitem____setitem__ 可以被定义/重载来做任何事情。它们甚至不必彼此一致。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-06
    • 1970-01-01
    • 1970-01-01
    • 2021-08-29
    • 1970-01-01
    • 1970-01-01
    • 2018-07-27
    相关资源
    最近更新 更多