002-NumPy数组基础
学习目标
通过本章节的学习,你将能够:
- 理解ndarray对象的核心概念
- 掌握数组与Python列表的区别
- 了解数组的内存布局和存储机制
- 学会创建和操作基本数组
- 理解数组的维度概念
1. ndarray对象简介
1.1 什么是ndarray
ndarray(N-dimensional array)是NumPy的核心数据结构,它是一个:
- 同质化数组:所有元素具有相同的数据类型
- 多维数组:支持任意维度的数组
- 高效存储:使用连续的内存布局
- 向量化运算:支持整个数组的批量操作
1.2 数组vs列表对比
# 文件路径: examples/array_vs_list.py
import numpy as np
import sys
def compare_array_list():
"""比较NumPy数组和Python列表"""
print("NumPy数组 vs Python列表对比")
print("=" * 50)
# 创建相同数据的列表和数组
python_list = [1, 2, 3, 4, 5]
numpy_array = np.array([1, 2, 3, 4, 5])
print("1. 数据类型")
print(f"Python列表: {type(python_list)}")
print(f"NumPy数组: {type(numpy_array)}")
print("\n2. 元素类型")
print(f"列表元素类型: {type(python_list[0])}")
print(f"数组元素类型: {numpy_array.dtype}")
print("\n3. 内存使用")
print(f"列表内存: {sys.getsizeof(python_list)} bytes")
print(f"数组内存: {numpy_array.nbytes} bytes")
print("\n4. 运算支持")
try:
# 列表运算(会报错)
# list_result = python_list * 2 # 这会重复列表
print("列表乘法: [1, 2, 3] * 2 =", [1, 2, 3] * 2)
# 数组运算(向量化)
array_result = numpy_array * 2
print("数组乘法: array * 2 =", array_result)
except Exception as e:
print(f"运算错误: {e}")
print("\n5. 性能对比")
import time
# 大数据量测试
size = 1000000
large_list = list(range(size))
large_array = np.arange(size)
# 列表求和
start = time.time()
list_sum = sum(large_list)
list_time = time.time() - start
# 数组求和
start = time.time()
array_sum = np.sum(large_array)
array_time = time.time() - start
print(f"列表求和时间: {list_time:.6f}秒")
print(f"数组求和时间: {array_time:.6f}秒")
print(f"性能提升: {list_time/array_time:.1f}倍")
if __name__ == "__main__":
compare_array_list()
1.3 数组的优势
特性 | Python列表 | NumPy数组 |
---|---|---|
数据类型 | 混合类型 | 同质类型 |
内存效率 | 较低 | 高效 |
计算速度 | 较慢 | 快速 |
向量化运算 | 不支持 | 支持 |
多维支持 | 需要嵌套 | 原生支持 |
数学函数 | 有限 | 丰富 |
广播机制 | 无 | 支持 |
2. 数组的维度概念
2.1 维度层次结构
2.2 各维度示例
# 文件路径: examples/array_dimensions.py
import numpy as np
def demonstrate_dimensions():
"""演示不同维度的数组"""
print("NumPy数组维度演示")
print("=" * 40)
# 0维数组(标量)
scalar = np.array(42)
print("0维数组(标量):")
print(f"值: {scalar}")
print(f"形状: {scalar.shape}")
print(f"维度: {scalar.ndim}")
print(f"大小: {scalar.size}")
print()
# 1维数组(向量)
vector = np.array([1, 2, 3, 4, 5])
print("1维数组(向量):")
print(f"值: {vector}")
print(f"形状: {vector.shape}")
print(f"维度: {vector.ndim}")
print(f"大小: {vector.size}")
print()
# 2维数组(矩阵)
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
print("2维数组(矩阵):")
print(f"值:\n{matrix}")
print(f"形状: {matrix.shape}")
print(f"维度: {matrix.ndim}")
print(f"大小: {matrix.size}")
print()
# 3维数组(张量)
tensor = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
print("3维数组(张量):")
print(f"值:\n{tensor}")
print(f"形状: {tensor.shape}")
print(f"维度: {tensor.ndim}")
print(f"大小: {tensor.size}")
print()
# 可视化3维数组结构
print("3维数组结构解析:")
print(f"第0个2D切片:\n{tensor[0]}")
print(f"第1个2D切片:\n{tensor[1]}")
if __name__ == "__main__":
demonstrate_dimensions()
2.3 形状(Shape)详解
# 文件路径: examples/array_shape.py
import numpy as np
def explore_array_shape():
"""探索数组形状的概念"""
print("数组形状(Shape)详解")
print("=" * 30)
# 不同形状的数组
arrays = [
np.array([1, 2, 3]), # (3,)
np.array([[1, 2, 3]]), # (1, 3)
np.array([[1], [2], [3]]), # (3, 1)
np.array([[1, 2], [3, 4], [5, 6]]), # (3, 2)
np.array([[[1, 2]], [[3, 4]]]), # (2, 1, 2)
]
for i, arr in enumerate(arrays, 1):
print(f"数组{i}:")
print(f"值: {arr if arr.ndim <= 2 else '3D数组'}")
if arr.ndim > 2:
print(f"详细值:\n{arr}")
print(f"形状: {arr.shape}")
print(f"维度: {arr.ndim}")
print(f"总元素数: {arr.size}")
print("-" * 20)
# 形状的含义
print("\n形状含义解释:")
arr_2d = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"2D数组:\n{arr_2d}")
print(f"形状 {arr_2d.shape} 表示:")
print(f" - 第0维(行): {arr_2d.shape[0]} 行")
print(f" - 第1维(列): {arr_2d.shape[1]} 列")
arr_3d = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
[[9, 10], [11, 12]]])
print(f"\n3D数组形状 {arr_3d.shape} 表示:")
print(f" - 第0维(深度): {arr_3d.shape[0]} 个2D数组")
print(f" - 第1维(行): {arr_3d.shape[1]} 行")
print(f" - 第2维(列): {arr_3d.shape[2]} 列")
if __name__ == "__main__":
explore_array_shape()
3. 数组的内存布局
3.1 内存存储方式
3.2 内存布局示例
# 文件路径: examples/memory_layout.py
import numpy as np
def explore_memory_layout():
"""探索数组的内存布局"""
print("数组内存布局探索")
print("=" * 30)
# 创建2D数组
arr = np.array([[1, 2, 3],
[4, 5, 6]])
print(f"原始数组:\n{arr}")
print(f"形状: {arr.shape}")
print(f"数据类型: {arr.dtype}")
print(f"总字节数: {arr.nbytes}")
print(f"每个元素字节数: {arr.itemsize}")
# 内存相关属性
print("\n内存属性:")
print(f"是否C连续: {arr.flags['C_CONTIGUOUS']}")
print(f"是否F连续: {arr.flags['F_CONTIGUOUS']}")
print(f"步长(strides): {arr.strides}")
# 步长的含义
print("\n步长含义:")
print(f"行步长: {arr.strides[0]} bytes (移动到下一行需要跳过的字节数)")
print(f"列步长: {arr.strides[1]} bytes (移动到下一列需要跳过的字节数)")
# 不同内存布局的数组
print("\n不同内存布局:")
# C-order (行优先)
arr_c = np.array([[1, 2, 3], [4, 5, 6]], order='C')
print(f"C-order数组步长: {arr_c.strides}")
# F-order (列优先)
arr_f = np.array([[1, 2, 3], [4, 5, 6]], order='F')
print(f"F-order数组步长: {arr_f.strides}")
# 内存视图
print("\n内存数据查看:")
print(f"C-order原始字节: {arr_c.tobytes()}")
print(f"F-order原始字节: {arr_f.tobytes()}")
def demonstrate_strides():
"""演示步长的工作原理"""
print("\n步长工作原理演示")
print("=" * 30)
arr = np.array([[10, 20, 30],
[40, 50, 60]], dtype=np.int32)
print(f"数组:\n{arr}")
print(f"步长: {arr.strides}")
print(f"数据类型大小: {arr.itemsize} bytes")
# 手动计算元素位置
print("\n元素内存位置计算:")
base_address = arr.__array_interface__['data'][0]
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
offset = i * arr.strides[0] + j * arr.strides[1]
print(f"arr[{i},{j}] = {arr[i,j]}, 内存偏移: {offset} bytes")
if __name__ == "__main__":
explore_memory_layout()
demonstrate_strides()
4. 数组的基本属性
4.1 核心属性总览
# 文件路径: examples/array_attributes.py
import numpy as np
def explore_array_attributes():
"""探索数组的各种属性"""
print("NumPy数组属性详解")
print("=" * 40)
# 创建示例数组
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]], dtype=np.float64)
print(f"示例数组:\n{arr}")
print("\n=== 基本属性 ===")
# 形状和维度
print(f"shape (形状): {arr.shape}")
print(f"ndim (维度数): {arr.ndim}")
print(f"size (元素总数): {arr.size}")
# 数据类型
print(f"\ndtype (数据类型): {arr.dtype}")
print(f"itemsize (元素字节数): {arr.itemsize}")
print(f"nbytes (总字节数): {arr.nbytes}")
# 内存信息
print(f"\n=== 内存信息 ===")
print(f"strides (步长): {arr.strides}")
print(f"flags (标志):")
for flag, value in arr.flags.items():
print(f" {flag}: {value}")
# 数据缓冲区信息
print(f"\n=== 缓冲区信息 ===")
print(f"data: {arr.data}")
print(f"base: {arr.base}")
# 实用属性
print(f"\n=== 实用属性 ===")
print(f"T (转置):")
print(arr.T)
print(f"\nflat (扁平迭代器): {list(arr.flat)}")
print(f"real (实部): {arr.real}")
print(f"imag (虚部): {arr.imag}")
def compare_dtypes():
"""比较不同数据类型的数组"""
print("\n\n数据类型对比")
print("=" * 30)
data = [1, 2, 3, 4, 5]
dtypes = [np.int8, np.int32, np.int64, np.float32, np.float64]
for dtype in dtypes:
arr = np.array(data, dtype=dtype)
print(f"{dtype.__name__:>8}: itemsize={arr.itemsize}, nbytes={arr.nbytes}")
if __name__ == "__main__":
explore_array_attributes()
compare_dtypes()
4.2 属性详细说明
属性 | 说明 | 示例 |
---|---|---|
shape | 数组各维度的大小 | (3, 4) |
ndim | 数组的维度数 | 2 |
size | 数组元素总数 | 12 |
dtype | 元素数据类型 | float64 |
itemsize | 每个元素的字节数 | 8 |
nbytes | 数组总字节数 | 96 |
strides | 各维度的步长 | (32, 8) |
flags | 内存布局标志 | C_CONTIGUOUS: True |
data | 数据缓冲区 | <memory at 0x...> |
base | 基础数组对象 | None 或 数组对象 |
5. 数组的基本操作
5.1 数组访问
# 文件路径: examples/array_access.py
import numpy as np
def demonstrate_array_access():
"""演示数组访问方法"""
print("数组访问方法演示")
print("=" * 30)
# 1维数组访问
arr_1d = np.array([10, 20, 30, 40, 50])
print(f"1D数组: {arr_1d}")
print(f"第一个元素: arr_1d[0] = {arr_1d[0]}")
print(f"最后一个元素: arr_1d[-1] = {arr_1d[-1]}")
print(f"切片: arr_1d[1:4] = {arr_1d[1:4]}")
# 2维数组访问
arr_2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(f"\n2D数组:\n{arr_2d}")
print(f"元素访问: arr_2d[1, 2] = {arr_2d[1, 2]}")
print(f"行访问: arr_2d[1] = {arr_2d[1]}")
print(f"列访问: arr_2d[:, 1] = {arr_2d[:, 1]}")
print(f"子矩阵: arr_2d[0:2, 1:3] =\n{arr_2d[0:2, 1:3]}")
# 3维数组访问
arr_3d = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
print(f"\n3D数组:\n{arr_3d}")
print(f"元素访问: arr_3d[1, 0, 1] = {arr_3d[1, 0, 1]}")
print(f"2D切片: arr_3d[0] =\n{arr_3d[0]}")
def demonstrate_array_modification():
"""演示数组修改"""
print("\n\n数组修改演示")
print("=" * 20)
arr = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(f"原始数组:\n{arr}")
# 单个元素修改
arr[1, 1] = 99
print(f"\n修改arr[1,1]=99后:\n{arr}")
# 行修改
arr[0] = [10, 20, 30]
print(f"\n修改第0行后:\n{arr}")
# 列修改
arr[:, 2] = [100, 200, 300]
print(f"\n修改第2列后:\n{arr}")
# 切片修改
arr[1:3, 0:2] = [[11, 22], [33, 44]]
print(f"\n修改子矩阵后:\n{arr}")
if __name__ == "__main__":
demonstrate_array_access()
demonstrate_array_modification()
5.2 数组复制
# 文件路径: examples/array_copy.py
import numpy as np
def demonstrate_array_copy():
"""演示数组复制的不同方式"""
print("数组复制方式对比")
print("=" * 30)
original = np.array([[1, 2, 3], [4, 5, 6]])
print(f"原始数组:\n{original}")
# 1. 赋值(共享内存)
assignment = original
print(f"\n1. 赋值操作")
print(f"assignment is original: {assignment is original}")
print(f"共享内存: {np.shares_memory(original, assignment)}")
# 修改赋值变量
assignment[0, 0] = 999
print(f"修改assignment后,原数组: \n{original}")
# 恢复原始值
original[0, 0] = 1
# 2. 视图(共享数据)
view = original.view()
print(f"\n2. 视图操作")
print(f"view is original: {view is original}")
print(f"共享内存: {np.shares_memory(original, view)}")
print(f"view.base is original: {view.base is original}")
# 修改视图
view[0, 1] = 888
print(f"修改view后,原数组: \n{original}")
# 恢复原始值
original[0, 1] = 2
# 3. 深拷贝(独立内存)
deep_copy = original.copy()
print(f"\n3. 深拷贝操作")
print(f"deep_copy is original: {deep_copy is original}")
print(f"共享内存: {np.shares_memory(original, deep_copy)}")
# 修改深拷贝
deep_copy[0, 2] = 777
print(f"修改deep_copy后,原数组: \n{original}")
print(f"deep_copy: \n{deep_copy}")
# 4. 切片(视图)
slice_view = original[0:1, :]
print(f"\n4. 切片操作")
print(f"切片结果: {slice_view}")
print(f"共享内存: {np.shares_memory(original, slice_view)}")
# 修改切片
slice_view[0, 0] = 666
print(f"修改切片后,原数组: \n{original}")
def memory_efficiency_demo():
"""演示内存效率"""
print("\n\n内存效率演示")
print("=" * 20)
# 创建大数组
large_array = np.random.random((1000, 1000))
print(f"大数组内存: {large_array.nbytes / 1024 / 1024:.2f} MB")
# 视图不占用额外内存
view = large_array[::2, ::2] # 每隔一个元素取样
print(f"视图形状: {view.shape}")
print(f"视图是否共享内存: {np.shares_memory(large_array, view)}")
# 拷贝占用额外内存
copy = large_array[::2, ::2].copy()
print(f"拷贝形状: {copy.shape}")
print(f"拷贝是否共享内存: {np.shares_memory(large_array, copy)}")
print(f"拷贝内存: {copy.nbytes / 1024 / 1024:.2f} MB")
if __name__ == "__main__":
demonstrate_array_copy()
memory_efficiency_demo()
6. 数组与标量的运算
6.1 向量化运算
# 文件路径: examples/vectorized_operations.py
import numpy as np
def demonstrate_vectorized_operations():
"""演示向量化运算"""
print("向量化运算演示")
print("=" * 30)
arr = np.array([[1, 2, 3],
[4, 5, 6]])
print(f"原始数组:\n{arr}")
# 数组与标量运算
print("\n数组与标量运算:")
print(f"arr + 10:\n{arr + 10}")
print(f"arr * 2:\n{arr * 2}")
print(f"arr ** 2:\n{arr ** 2}")
print(f"arr / 2:\n{arr / 2}")
# 数学函数
print("\n数学函数:")
print(f"np.sqrt(arr):\n{np.sqrt(arr)}")
print(f"np.sin(arr):\n{np.sin(arr)}")
print(f"np.exp(arr):\n{np.exp(arr)}")
# 比较运算
print("\n比较运算:")
print(f"arr > 3:\n{arr > 3}")
print(f"arr == 5:\n{arr == 5}")
# 逻辑运算
print("\n逻辑运算:")
condition1 = arr > 2
condition2 = arr < 6
print(f"(arr > 2) & (arr < 6):\n{condition1 & condition2}")
def compare_with_loops():
"""比较向量化运算与循环的性能"""
print("\n\n向量化 vs 循环性能对比")
print("=" * 30)
import time
# 创建大数组
size = 1000000
arr = np.random.random(size)
# 向量化运算
start = time.time()
result_vectorized = arr ** 2 + np.sin(arr)
vectorized_time = time.time() - start
# 循环运算
start = time.time()
result_loop = np.zeros(size)
for i in range(size):
result_loop[i] = arr[i] ** 2 + np.sin(arr[i])
loop_time = time.time() - start
print(f"向量化运算时间: {vectorized_time:.6f}秒")
print(f"循环运算时间: {loop_time:.6f}秒")
print(f"性能提升: {loop_time/vectorized_time:.1f}倍")
# 验证结果一致性
print(f"结果一致性: {np.allclose(result_vectorized, result_loop)}")
if __name__ == "__main__":
demonstrate_vectorized_operations()
compare_with_loops()
实践练习
练习1:数组属性探索
创建不同维度的数组,探索它们的各种属性。
# 练习提示
# TODO: 创建以下数组并分析其属性
# 1. 1D数组: [1, 2, 3, 4, 5]
# 2. 2D数组: 3x4的矩阵
# 3. 3D数组: 2x3x4的张量
# 分析每个数组的shape, ndim, size, dtype, strides等属性
练习2:内存布局实验
比较C-order和F-order数组的性能差异。
# 练习提示
# TODO: 实现以下功能
# 1. 创建相同数据的C-order和F-order数组
# 2. 比较行访问和列访问的性能
# 3. 分析内存布局对性能的影响
练习3:向量化运算应用
实现一个简单的图像处理函数,使用向量化运算。
# 练习提示
# TODO: 实现以下功能
# 1. 创建一个模拟的灰度图像(2D数组)
# 2. 实现亮度调整(加法运算)
# 3. 实现对比度调整(乘法运算)
# 4. 实现图像反转(255 - 像素值)
总结
通过本章节的学习,你已经:
✅ 理解了ndarray的核心概念:同质化、多维、高效的数组对象
✅ 掌握了数组与列表的区别:性能、功能、内存效率等方面
✅ 学会了数组的维度概念:从0维标量到N维张量
✅ 了解了内存布局机制:C-order、F-order、步长等概念
✅ 掌握了数组的基本操作:访问、修改、复制等
✅ 体验了向量化运算的威力:高效的批量计算
关键要点
- ndarray是NumPy的核心:理解其特性是使用NumPy的基础
- 向量化运算是关键优势:避免显式循环,提高性能
- 内存布局影响性能:了解步长和连续性概念
- 视图vs拷贝的区别:合理使用可以节省内存
下一步学习
在下一章节中,我们将学习NumPy数组的创建方法,包括:
- 各种数组创建函数
- 从现有数据创建数组
- 特殊数组的生成
- 数组初始化的最佳实践
💡 学习建议: 多动手实践本章的代码示例,特别是内存布局和向量化运算的部分。
下一步
003-数组创建方法 - 学习创建NumPy数组的各种方法
更新记录:
- 2024-01-15: 创建文档
- 版本: v1.0.0