【问题标题】:is there a rule of thumb to know if i'm modifying a value or a referenced value?是否有经验法则可以知道我是在修改值还是引用值?
【发布时间】:2020-11-19 14:02:57
【问题描述】:

考虑以下几点:

from scipy.spatial import ConvexHull
import numpy as np

pts = np.random.rand(30, 2)
hull = ConvexHull(pts)
foo = hull.points
foo[0] = 4
print(pts[0]) # -> [4. 4.]
bar = foo[0]
bar[0] = 8    
print(pts[0]) # -> [8. 4.]

我怎么知道修改 hull.points(或 foo,对 hull.points 的引用)正在修改 pts

文档只说:

points: ndarray of double, shape (npoints, ndim)
        Coordinates of input points.

pycharm 中的检查器还告诉我 foo 和 hull.points 都是 ndarray 并且代码、文档中没有任何内容,检查器告诉我我的变量实际上是引用相同值的指针(是的,我来自C,对不起)

它可能会很快出错,因为如果我直接修改“pts”的单个元素(包含我的其他变量/指针引用的所有值的二维数组)它也会修改我的所有变量和我的凸包“条”不再是凸的:

...
pts[0] = 16
print(bar) # -> [16. 16.]

除非我再次打电话

pts = np.random.rand(30, 2)
print(bar) # -> [16. 16.]
print(pts[0]) # -> [some random value]

pts 显然在新的内存位置变成了一个完全不同的对象,因此在这种特定情况下,bar 不再是对 pts 的引用。

而且它可以永远持续下去:如果我现在修改 foo = hull.points 那么 bar 不再是对 foo 的引用(嗯......它是对“旧 foo”的引用,不再可以访问了)

我的用例:所有参数都传递并返回不同方法的值:我正在失去对所有参考和值的跟踪。我最终在不知不觉中返回了一个值列表,尽管我修改了(也在不知不觉中)一个引用(因此是原始的ndarray),我什至不知道我的返回值是一个引用还是一个独立的对象安全修改,不会弄乱其他所有内容。

完全简化的用例:

from scipy.spatial import ConvexHull
import numpy as np

pts = np.random.rand(30, 2)
hull = ConvexHull(pts)
foo = hull.points
foo[0] = 4
print(pts[0]) # -> [4. 4.]
bar = foo[0]
bar[0] = 8    
print(pts[0]) # -> [8. 4.]

qux = bar[0]  # WOOPS /!\ qux isn't a reference to an ndarray element, it's just a "float" value
bar[0] = 16   # BUT  this is a reference so i end up modifying pts BUT not qux /!\

print(pts[0], foo[0], bar, qux) # -> [16.  4.] [16.  4.] [16.  4.] 8.0 

qux = bar      # /!\ qux is now again a reference to pts
qux[0] = 128
print(pts[0], foo[0], bar, qux) # -> [128.   4.] [128.   4.] [128.   4.] [128.   4.]

qux = foo[0]   # remember that qux = bar[0] didn't create a reference ?
qux[0] = 256   # but in this case, it is ! bar[0] is just a single float value while foo[0] is a reference to a ndarray
print(pts[0], foo[0], bar, qux) # -> [256.   4.] [256.   4.] [256.   4.] [256.   4.]

因为这非常“有趣”。现在我有 qux 和 bar 引用 foo[0],如果我说 foo = None qux 和 bar 会发生什么?没什么... qux 一个栏仍然引用 pts[0] 即使我从未明确说过...我很迷茫。

我也想知道我是否不是特殊情况,因为它是 numpy/scipy/ndarray。我以前从来没有这样挣扎过。 (我很幸运?)

【问题讨论】:

    标签: python scipy numpy-ndarray


    【解决方案1】:

    在 python 中,变量总是引用。但是存储值类型的性质定义了它的行为。变量可以是可变的不可变的。可变对象是:int、float、str、tuple 等。不可变对象是大多数集合:dict、list、set 等。

    考虑这个例子:

    a, b, c = 1, 2, 3
    my_list = [a, b, c]
    new_list = my_list
    
    new_list[0] = 0
    
    >>> print(a, b, c)
    ... 1 2 3
    
    >>> print(my_list)
    ... [0, 2, 3]
    
    >>> print(new_list)
    ... [0, 2, 3]
    

    这里发生了什么:您确实更改了 my_list 的第 0 个元素。但是因为它是int 并且是不可变的,所以第 0 个元素将被赋予新的值,因此将是一个新的引用。然而a 仍将指向与以前相同的值。

    这基本上是不可变对象的想法:更改它会在内存中创建一个新对象并更新指针以指向这个新对象。因此,当您执行a += 1 时,实际上您创建了一个新的int 对象并将a 设置为指向该新对象。

    但是my_listlist 并且是可变的。所以改变它不会改变参考。这样,当您执行new_list = my_list 时,您将创建变量new_list,该变量引用与my_list 相同的对象。所以改变一个会改变另一个。


    变量本身永远不会保存值,它始终是一个引用。但是改变变量并不意味着改变引用的对象。对于不可变对象,改变值就是改变引用,对于可变对象,改变值就是改变它的内容。但是从来没有一个可变对象不是某种集合。因此,当您更改列表的可变项时,列表保持不变,但该项的引用更改为 new 引用。这样列表的内容会发生变化,但实际的列表对象保持不变。

    基本上,python 中的任何数据结构都可以向下钻取为不可变。它是int的列表吗?好吧,有你的不可变对象。它是int列表的列表吗?更深一层还有不可变的。它是一个类实例吗?我敢打赌它有字段,并且字段与任何其他数据结构没有什么不同。你明白了。

    这是另一个例子:

    a, b = 1, 2
    my_list = [a, b]
    
    >>> print(id(a), id(b))
    ... 4325931056 4325931088
    
    >>> print(id(my_list[0]), id(my_list[1]))
    ... 4325931056 4325931088
    
    >>> my_list[0] += 10
    >>> print(id(a), id(b))
    ... 4325931056 4325931088  # a still has the same reference
    
    >>> print(id(my_list[0]), id(my_list[1]))
    ... 4325931376 4325931088  # my_list[0] now has a new reference 
    
    >>> b += 10
    >>> print(id(b))
    ... 4325931408  # b now has a new reference
    
    >>> print(id(my_list[1]))
    ... 4325931088  # my_list[1] reference is still the same
    

    【讨论】:

    • 你是说它总是一个引用,除非它是不可变的,那么它就是一个值?我几乎明白了... my_list 不引用 a,b,c 否则会有一种方法通过修改另一个来修改一个(我试过)。除非修改创建一个新对象,即使所述对象是可变的,例如按照您的示例: my_list = [c,b,a] 然后引用 new_list -> my_list 丢失。 (new_list 仍然包含 [0,2,3])
    • 谢谢,我想我明白了。我明天再检查一下,然后尝试一些东西。我发现对于初学者友好的语言来说很难理解。它甚至不是什么奇怪的边缘情况,而是语言的基础。
    • 只搜索一些文档和文章。这并不复杂,只有在极少数情况下才会引人注目。
    【解决方案2】:

    另一个答案描述了基本的 Python 上下文 - 一切都是对象,变量是对对象的引用,有些对象是不可变的,而另一些是可变的。

    我将详细说明您的示例,因为 numpy 添加了一些细微差别。

    创建一个数组并将其分配给变量pts

    In [71]: pts = np.random.rand(30,2)
    In [72]: id(pts)
    Out[72]: 139777500963904
    

    ConvexHullscipy 导入。不看它的文档或代码,我推断它是一个类定义:

    #hull = ConvexHull(pts)
    #foo = hull.points
    

    hull 可能是一个ConvexHull 实例,在初始化的某个时刻有一个:

    self.points = pts
    

    换句话说,输入参数被分配(通过引用)points 属性。根据标准 python 实践,这只是一个参考分配,没有任何复制。

    你做得这么有效:

    In [73]: foo = pts
    In [74]: id(foo)
    Out[74]: 139777500963904             # same array object created by the rand call
    

    pts[0] 是一个返回数组的view 的索引。由于pts是二维的,所以也可以写成pts[0,:]

    In [75]: pts[0]
    Out[75]: array([0.79172504, 0.52889492])
    In [76]: foo[0]
    Out[76]: array([0.79172504, 0.52889492])    # same values
    

    列表包含引用,因此索引会返回其中一个对象。数组的索引更复杂。除非是object dtype,否则索引会返回一个新数组,该数组可能引用原始数组的值,也可能是副本。

    由于foopts是同一个数组,所以当我们查看pts时也会看到foo的这种修改:

    In [77]: foo[0] = 4
    In [78]: foo[0]
    Out[78]: array([4., 4.])
    In [79]: pts[0]
    Out[79]: array([4., 4.])
    

    我们可以将foo/pts 中的view 分配给变量。同样,这只是一个变量赋值,没有新副本。

    In [80]: bar = foo[0]
    In [81]: bar
    Out[81]: array([4., 4.])
    

    有时__array_interface__ 是一种检查某事物是view 还是copy 的便捷方式:

    In [82]: pts.__array_interface__
    Out[82]: 
    {'data': (43476672, False),
     'strides': None,
     'descr': [('', '<f8')],
     'typestr': '<f8',
     'shape': (30, 2),
     'version': 3}
    In [83]: bar.__array_interface__
    Out[83]: 
    {'data': (43476672, False),
     'strides': None,
     'descr': [('', '<f8')],
     'typestr': '<f8',
     'shape': (2,),
     'version': 3}
    

    解释这可能很棘手,但这里data 行具有相同的数字。作为粗略的“c”并行,该数字是指向数组使用的数据缓冲区的指针。这里barpts 使用相同的数据缓冲区,但bar 只能查看其中的两个元素(16 个字节),而不是全部。

    In [84]: bar.shape
    Out[84]: (2,)
    

    更改bar 的元素会更改foo/pts 数组的对应元素。

    In [85]: bar[0] = 0
    In [86]: bar
    Out[86]: array([0., 4.])
    In [87]: foo[0]
    Out[87]: array([0., 4.])
    

    在回答您最初的问题时,假设在改变数组(和列表)元素时,您可能会更改它派生自的数组的值。简单的变量赋值不会产生副本(适用于传入/传出函数的变量)。 numpy 使用view 尽可能避免不必要的复制,但对于某些索引操作,复制是不可避免的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-30
      • 1970-01-01
      • 1970-01-01
      • 2014-04-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-04
      相关资源
      最近更新 更多