numpy-深拷贝与视图

Posted by Hilda on July 15, 2025

1.完全没有复制(引用赋值)

当将一个 NumPy 数组直接赋值给另一个变量时,实际上并没有创建一个新的数组对象,而是创建了一个指向相同内存地址的新引用。这意味着两个变量现在都指向内存中的同一个 NumPy 数组对象。它们是“命运共同体”,对其中任何一个变量所代表的数组进行的修改,都会立即反映在另一个变量上,因为它们操作的是同一块数据。

这种行为与 Python 中可变对象的默认赋值行为是一致的。例如,列表、字典等可变对象在赋值时也是传递引用。

在 Python 中,变量是名称,它们绑定到内存中的对象。当执行 b = a 时,Python 并没有复制 a 所指向的对象,而是让 b 这个名称也指向 a 原本指向的那个对象。

  • 内存地址共享: ab 都存储了指向同一块内存区域的地址。
  • Python 的 is 运算符可以看作是对 id(a) == id(b) 的简化和封装,直接比较对象的身份而无需显式调用 id() 函数。
  • is 运算符: is 运算符用于检查两个变量是否指向内存中的同一个对象。如果 a is b 返回 True,则表示 ab 是同一个对象。
  • id() 函数: id() 函数返回对象的唯一标识符(通常是其内存地址)。如果 id(a) == id(b),则 ab 是同一个对象。
  • 可变性: NumPy 数组是可变对象。这意味着它们的内容可以在创建后被修改。由于 ab 指向同一个可变对象,因此通过 ab 修改对象内容都会影响到另一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arr = np.random.randint(0, 10, (3, 4))
arr1 = arr
display(id(arr), id(arr1))  # 都是4607038192
# is 检查两个变量是否指向内存中的同一个对象
display(arr1 is arr)   # True

arr1[0, 0] = 100
"""
array([[100,   5,   7,   5],
       [  5,   7,   2,   0],
       [  3,   4,   5,   1]])
"""
display(arr)

display(id(arr), id(arr1))  # 都是4607038192
# is 检查两个变量是否指向内存中的同一个对象
display(arr1 is arr)   # True

选择题

  1. 给定以下代码:

    1
    2
    3
    4
    
    import numpy as np
    arr1 = np.array([1, 2, 3])
    arr2 = arr1
    arr2[0] = 99
    

    执行上述代码后,arr1 的值是什么? A. [1, 2, 3] B. [99, 2, 3] C. [99, 99, 99] D. 报错

  2. 以下哪种情况会使得 a is b 返回 True? A. a = np.array([1, 2]); b = np.array([1, 2]) B. a = np.array([1, 2]); b = a.copy() C. a = np.array([1, 2]); b = a.view() D. a = np.array([1, 2]); b = a

答案:1.B,arr2 = arr1 使得 arr1arr2 指向同一个数组对象。对 arr2 的修改会直接影响到 arr1

2.D,a = np.array([1, 2]); b = a,只有直接赋值操作会使得两个变量指向同一个对象

2.查看 或 浅拷贝(View)

“视图”或“浅拷贝”是指创建一个新的数组对象,但这个新对象与原始数组共享相同的数据内存。这意味着:

  • 独立的数组对象: ab 是两个不同的 NumPy 数组对象,a is b 会返回 False
  • 共享数据: 尽管对象不同,但它们底层的数据存储是同一份。因此,对视图数组的修改会直接影响到原始数组,反之亦然。
  • view() 方法: 这是显式创建视图的方法。
  • 切片操作: 在 NumPy 中,切片操作(例如 a[0:2])通常会返回原始数组的一个视图,而不是一个副本。这是一个非常重要的特性,也是初学者容易混淆的地方。

NumPy 数组由两部分组成:

  1. 数组对象(Array Object): 包含数组的元数据(如 shapedtypestrides 等)以及一个指向实际数据内存块的指针。
  2. 数据缓冲区(Data Buffer): 实际存储数组元素值的内存区域。

当创建一个视图时:

  • 新的数组对象: NumPy 会创建一个全新的数组对象。这个新对象有自己的 shapedtypestrides 等元数据。
  • 共享数据指针: 这个新的数组对象中的数据指针会指向原始数组的数据缓冲区。它们没有自己的数据缓冲区。
  • base 属性: 视图数组有一个 base 属性,它指向拥有实际数据内存的原始数组对象。如果一个数组是另一个数组的视图,那么它的 base 属性将是非 None 的,并且指向拥有数据内存的那个数组。如果一个数组拥有自己的数据,那么它的 base 属性将是 None
  • flags.owndata 属性: 这是一个布尔标志,指示数组是否拥有其数据。如果 flags.owndataTrue,则数组拥有自己的数据。如果为 False,则数组是另一个数组的视图(或以其他方式共享数据)。

这种机制使得 NumPy 在处理大型数据集时非常高效,因为它避免了不必要的数据复制。

1
2
3
4
5
6
7
8
a = np.random.randint(0, 10, (1, 3))
print(id(a), a.flags.owndata, a.base)   # 4608066736 True None
# 创建视图
b = a.view()
print(id(b), b.flags.owndata, b.base)   # 4608068176 False [[6 2 1]]
print(a is b, b.base is a)  # False True
b[0, 0] = 100
print(b[0, 0], a[0, 0])   # 100 100

切片操作通常返回视图:

1
2
3
4
5
6
7
8
9
10
11
a = np.random.randint(0, 10, (4, 5))
# 切片
b = a[1:3, 0:2]
display(a, b)
print(f"a的内存地址:{id(a)}, b的内存地址:{id(b)}")  # 不同
print(b.base is a)  # True
print(b.flags.owndata)  # False   表示:不拥有自己的数据
# 修改切片中的元素
b[0, 0] = 100
print(b[0, 0])  # 100
print(a)  # 响应也会发生变化

image-20250715124031722


选择题

  1. 给定以下代码:

    1
    2
    3
    4
    
    import numpy as np
    arr1 = np.array([1, 2, 3, 4, 5])
    arr2 = arr1[1:4]
    arr2[0] = 99
    

    执行上述代码后,arr1 的值是什么?

    A. [1, 2, 3, 4, 5] B. [1, 99, 3, 4, 5] C. [99, 2, 3, 4, 5] D. [1, 99, 99, 99, 5]

    答案:B

  2. 以下关于 NumPy 视图的描述,哪一项是错误的?

    A. 视图是原始数组的一个独立对象。

    B. 视图和原始数组共享底层数据。

    C. 对视图的修改会影响原始数组。

    D. 视图拥有自己的数据副本。

答案:D

编程题

编写一段 Python 代码,创建一个 NumPy 数组 data

data 中创建一个切片 sub_data

验证 datasub_data 是否是不同的对象,但共享相同的数据。

修改 sub_data 中的一个元素,并打印 datasub_data,以显示修改的效果。

打印 sub_data.basesub_data.flags.owndata 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np

data = np.arange(1, 10).reshape(3, 3)
sub_data = data[0:2, 0:2]
print(f"原始数组:\n{data}")
print(f"切片:\n{sub_data}") 
print(sub_data.base is data)  # False
print(sub_data is data)  # False
sub_data[0, 0] = 99
print(f"原始数组:\n{data}")
print(f"切片:\n{sub_data}")
print(f"切片不拥有自己的数据:{sub_data.flags.owndata}")  # False
print(sub_data.base is data)  # False

image-20250715133819486

3.深拷贝

“深拷贝”是指创建一个完全独立的数组副本。这意味着新数组拥有自己独立的内存空间来存储数据,与原始数组没有任何关联。对深拷贝数组的任何修改都不会影响到原始数组,反之亦然。它们是“分道扬镳”的。

  • copy() 方法: 这是执行深拷贝的方法。
  • 独立的数据: 新数组会分配新的内存空间,并将原始数组的所有数据复制到这个新空间中。

当执行深拷贝时:

  • 新的数组对象: NumPy 会创建一个全新的数组对象。
  • 新的数据缓冲区: NumPy 会为这个新数组分配一块全新的内存区域
  • 数据复制: 原始数组中的所有元素值都会被复制到新分配的内存区域中。
  • 完全独立: 原始数组和深拷贝数组在内存中是完全独立的实体。它们各自拥有自己的数据,互不影响。
  • base 属性: 深拷贝数组的 base 属性将是 None,因为它不依赖于任何其他数组的数据。
  • flags.owndata 属性: 深拷贝数组的 flags.owndata 将是 True,因为它拥有自己的数据。

深拷贝通常需要一个完全独立的数据副本,并且不希望对副本的修改影响到原始数据时使用。虽然它提供了数据隔离,但由于需要分配新内存和复制数据,因此会比视图操作消耗更多的内存和计算资源,尤其是在处理大型数组时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arr = np.arange(0, 10)
print(f"原始数组:{arr}")
print(f"原始数组的内存地址:{id(arr)}")
print(f"原始数组是否拥有自己的数据:{arr.flags.owndata}")   # True
print(f"原始数组的base属性:{arr.base}")  # None
# 使用copy方法创建一个深拷贝
b = arr.copy()
print(f"拷贝数组:{b}")
print(f"拷贝数组的内存地址:{id(b)}")
print(f"拷贝数组是不是原始数组:{b is arr}")
print(f"拷贝数据的base属性:{b.base}")  # None   因为b有自己的数据
print(f"拷贝数组是否拥有自己的数据:{b.flags.owndata}")  # True

# 修改b中的元素
b[0] = 1000
print(f"原始数组:{arr}")  #  不变
print(f"拷贝数组:{b}")

image-20250715134602077

补充:切片后进行深拷贝,以释放内存

1
2
3
4
5
6
7
8
9
10
11
12
# 假设a是一个巨大的中间结果
a_large = np.arange(1e8)
print(f"a的大小(字节):{a_large.nbytes}")  # 800000000
# 从a_large中取出一小部分进行深拷贝
# 如果不copy(),b_small将是a_large的视图,a_large无法被垃圾回收
b_small = a_large[::1000000].copy()  # 每100万个数据中取一个数据,进行深拷贝
print(f"b_small的大小(字节):{b_small.nbytes}")  # 800
# 删除不再需要的大型数组a_large  释放其占用的内存
# 如果b_small是其视图,del a_large并不能真正释放内存,因为b_small还在使用它
del a_large
print("大型数组a_large已经删除")
print(f"b_small依然可以使用:{b_small}")

image-20250715135636007

选择题

  1. 给定以下代码:

    1
    2
    3
    4
    
    import numpy as np
    arr1 = np.array([10, 20, 30])
    arr2 = arr1.copy()
    arr2[1] = 50
    

    执行上述代码后,arr1arr2 的值分别是什么?

    A. arr1 = [10, 20, 30], arr2 = [10, 50, 30]

    B. arr1 = [10, 50, 30], arr2 = [10, 50, 30]

    C. arr1 = [10, 20, 30], arr2 = [10, 20, 30]

    D. arr1 = [10, 50, 30], arr2 = [10, 20, 30]

    答案:A

  2. 以下哪种情况下,对 b 的修改不会影响到 a

    A. a = np.array([1, 2]); b = a

    B. a = np.array([1, 2]); b = a.view()

    C. a = np.array([1, 2]); b = a.copy()

    D. a = np.array([1, 2]); b = a[0:1]

    答案:C

编程题

编写一段 Python 代码,创建一个 NumPy 数组 original_array

创建一个 original_array 的深拷贝 copied_array

修改 copied_array 中的一个元素。

打印 original_arraycopied_array,以证明它们是独立的。

验证 copied_array.base 是否为 None,以及 copied_array.flags.owndata 是否为 True

1
2
3
4
5
6
7
8
9
original_array = np.arange(0, 10)
print(original_array)
copied_array = original_array.copy()
print(copied_array)
copied_array[0] = 999
print(original_array)
print(copied_array)
print(copied_array.base)  # None
print(copied_array.flags.owndata)  # True

image-20250715140615006