1.完全没有复制(引用赋值)
当将一个 NumPy 数组直接赋值给另一个变量时,实际上并没有创建一个新的数组对象,而是创建了一个指向相同内存地址的新引用。这意味着两个变量现在都指向内存中的同一个 NumPy 数组对象。它们是“命运共同体”,对其中任何一个变量所代表的数组进行的修改,都会立即反映在另一个变量上,因为它们操作的是同一块数据。
这种行为与 Python 中可变对象的默认赋值行为是一致的。例如,列表、字典等可变对象在赋值时也是传递引用。
在 Python 中,变量是名称,它们绑定到内存中的对象。当执行
b = a
时,Python 并没有复制a
所指向的对象,而是让b
这个名称也指向a
原本指向的那个对象。
- 内存地址共享:
a
和b
都存储了指向同一块内存区域的地址。- Python 的
is
运算符可以看作是对id(a) == id(b)
的简化和封装,直接比较对象的身份而无需显式调用 id() 函数。is
运算符:is
运算符用于检查两个变量是否指向内存中的同一个对象。如果a is b
返回True
,则表示a
和b
是同一个对象。id()
函数:id()
函数返回对象的唯一标识符(通常是其内存地址)。如果id(a) == id(b)
,则a
和b
是同一个对象。- 可变性: NumPy 数组是可变对象。这意味着它们的内容可以在创建后被修改。由于
a
和b
指向同一个可变对象,因此通过a
或b
修改对象内容都会影响到另一个。
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 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. 报错 -
以下哪种情况会使得
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
使得arr1
和arr2
指向同一个数组对象。对arr2
的修改会直接影响到arr1
。2.D,
a = np.array([1, 2]); b = a
,只有直接赋值操作会使得两个变量指向同一个对象
2.查看 或 浅拷贝(View)
“视图”或“浅拷贝”是指创建一个新的数组对象,但这个新对象与原始数组共享相同的数据内存。这意味着:
- 独立的数组对象:
a
和b
是两个不同的 NumPy 数组对象,a is b
会返回False
。 - 共享数据: 尽管对象不同,但它们底层的数据存储是同一份。因此,对视图数组的修改会直接影响到原始数组,反之亦然。
view()
方法: 这是显式创建视图的方法。- 切片操作: 在 NumPy 中,切片操作(例如
a[0:2]
)通常会返回原始数组的一个视图,而不是一个副本。这是一个非常重要的特性,也是初学者容易混淆的地方。
NumPy 数组由两部分组成:
- 数组对象(Array Object): 包含数组的元数据(如
shape
、dtype
、strides
等)以及一个指向实际数据内存块的指针。- 数据缓冲区(Data Buffer): 实际存储数组元素值的内存区域。
当创建一个视图时:
- 新的数组对象: NumPy 会创建一个全新的数组对象。这个新对象有自己的
shape
、dtype
、strides
等元数据。- 共享数据指针: 这个新的数组对象中的数据指针会指向原始数组的数据缓冲区。它们没有自己的数据缓冲区。
base
属性: 视图数组有一个base
属性,它指向拥有实际数据内存的原始数组对象。如果一个数组是另一个数组的视图,那么它的base
属性将是非None
的,并且指向拥有数据内存的那个数组。如果一个数组拥有自己的数据,那么它的base
属性将是None
。flags.owndata
属性: 这是一个布尔标志,指示数组是否拥有其数据。如果flags.owndata
为True
,则数组拥有自己的数据。如果为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) # 响应也会发生变化
选择题
-
给定以下代码:
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
-
以下关于 NumPy 视图的描述,哪一项是错误的?
A. 视图是原始数组的一个独立对象。
B. 视图和原始数组共享底层数据。
C. 对视图的修改会影响原始数组。
D. 视图拥有自己的数据副本。
答案:D
编程题
编写一段 Python 代码,创建一个 NumPy 数组 data
。
从 data
中创建一个切片 sub_data
。
验证 data
和 sub_data
是否是不同的对象,但共享相同的数据。
修改 sub_data
中的一个元素,并打印 data
和 sub_data
,以显示修改的效果。
打印 sub_data.base
和 sub_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
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}")
补充:切片后进行深拷贝,以释放内存
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}")
选择题
-
给定以下代码:
1 2 3 4
import numpy as np arr1 = np.array([10, 20, 30]) arr2 = arr1.copy() arr2[1] = 50
执行上述代码后,
arr1
和arr2
的值分别是什么?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
-
以下哪种情况下,对
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_array
和 copied_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