
1.2 《ndarray解剖课:多维数组的底层实现》
内容介绍
NumPy 的 ndarray 是其核心数据结构,用于高效处理多维数组。在这篇文章中,我们将深入解析 ndarray 的底层实现,探讨其内存结构、维度、数据类型、步长等关键概念,并通过实验验证这些概念的实际应用。
1.2.1 ndarray与Python列表的核心差异
ndarray 和 Python 列表是两种不同的数据结构,它们在内存布局和性能上有显著的差异。下面是 ndarray 和 Python 列表的核心差异对比表:
| 特性 | ndarray | Python 列表 | 
|---|---|---|
| 内存布局 | 连续的内存块(固定大小) | 动态分配的内存(指向对象的指针) | 
| 数据类型 | 统一的数据类型(dtype) | 混合的数据类型(可以包含任意类型的对象) | 
| 访问速度 | 高效的向量化操作 | 较慢的迭代访问 | 
| 修改成本 | 低(视图和副本) | 高(需要重新分配内存) | 
| 支持的运算 | 广泛的数学和科学计算功能 | 有限的列表操作 | 
| 数据对齐 | 自动对齐(通过步长) | 无对齐 | 
| 计算性能 | 高(利用C/C++实现) | 低(纯Python实现) | 
| 文件读写 | 支持 .npy 和 .npz 文件格式 | 不支持二进制文件格式,需要额外的库支持 | 
| 集成性 | 与 Pandas、Scikit-learn 等科学计算库高度集成 | 与标准库高度集成,但与其他科学计算库集成度较低 | 
1.2.2 ndarray内存结构3D示意图
为了更好地理解 ndarray 的内存结构,我们绘制一个 3D 示意图,展示 ndarray 如何在内存中存储多维数组。
内存布局示意图(三维数组示例)
| 内存地址 | 0x1000 | 0x1004 | 0x1008 | 0x100C | 0x1010 | 0x1014 | … | 
|---|---|---|---|---|---|---|---|
| 三维索引 | [0,0,0] | [0,0,1] | [0,1,0] | [0,1,1] | [1,0,0] | [1,0,1] | … | 
| 二维展开 | [0,0] | [0,1] | [1,0] | [1,1] | [2,0] | [2,1] | … | 
| 一维展开 | 0 | 1 | 2 | 3 | 4 | 5 | … | 
内存布局验证实验
import numpy as np
# 创建基础数组
base_arr = np.arange(6, dtype=np.int32)
print(f"原始数组ID: {id(base_arr)}")  # 输出原始数组内存地址
# 创建视图
view_arr = base_arr[::2]  # 步长切片创建视图
print(f"视图数组ID: {id(view_arr)}")  # 地址不同但共享数据
# 创建副本
copy_arr = base_arr.copy()  # 完整内存复制
print(f"副本数组ID: {id(copy_arr)}")  # 全新内存地址
# 修改视图影响原始数组
view_arr[0] = 100
print("修改视图后的原始数组:", base_arr)  # 输出[100  1   2   3   4   5]
1.2.3 维度(shape)、数据类型(dtype)、步长(strides)的关联关系
ndarray 的三个关键属性是 shape(维度)、dtype(数据类型)和 strides(步长)。它们之间的关系如下:
- shape:表示数组的形状,即每个维度的大小。例如,shape=(3, 3)表示一个 3x3 的二维数组。
- dtype:表示数组中每个元素的数据类型。例如,dtype=np.float64表示数组中的元素是 64 位浮点数。
- strides:表示在内存中从一个元素移动到下一个元素所需的字节数。例如,在一个 shape=(3, 3)、dtype=np.float64的数组中,步长strides=(24, 8)表示从一个行到下一个行需要移动 24 个字节,从一个列到下一个列需要移动 8 个字节。
步长计算公式推导:
对于形状为 
     
      
       
       
         ( 
        
        
        
          d 
         
        
          1 
         
        
       
         , 
        
        
        
          d 
         
        
          2 
         
        
       
         , 
        
       
         . 
        
       
         . 
        
       
         . 
        
       
         , 
        
        
        
          d 
         
        
          n 
         
        
       
         ) 
        
       
      
        (d_1,d_2,...,d_n) 
       
      
    (d1,d2,...,dn)的数组,第 
     
      
       
       
         k 
        
       
      
        k 
       
      
    k维步长:
  
      
       
        
        
          s 
         
        
          t 
         
        
          r 
         
        
          i 
         
        
          d 
         
         
         
           e 
          
         
           k 
          
         
        
          = 
         
         
         
           ( 
          
          
          
            ∏ 
           
           
           
             i 
            
           
             = 
            
           
             k 
            
           
             + 
            
           
             1 
            
           
          
            n 
           
          
          
          
            d 
           
          
            i 
           
          
         
           ) 
          
         
        
          × 
         
        
          i 
         
        
          t 
         
        
          e 
         
        
          m 
         
        
          s 
         
        
          i 
         
        
          z 
         
        
          e 
         
        
       
         stride_k = \left( \prod_{i=k+1}^{n} d_i \right) \times itemsize 
        
       
     stridek=(i=k+1∏ndi)×itemsize
示例:三维数组(2,3,4),数据类型int32(4字节)
axis0_stride = 3*4*4 = 48 字节
axis1_stride = 4*4 = 16 字节
axis2_stride = 4 字节
1.2.4 不同初始化方式的内存分配对比(zeros vs empty)
NumPy 提供了多种初始化数组的方法,其中 np.zeros 和 np.empty 是两个常用的方法。我们将通过实验对比它们的内存分配方式。
import numpy as np
# 创建一个 3x3 的零数组
zeros_array = np.zeros((3, 3), dtype=np.float64)
print("零数组:")
print(zeros_array)
# 创建一个 3x3 的未初始化数组
empty_array = np.empty((3, 3), dtype=np.float64)
print("未初始化数组:")
print(empty_array)
# 验证两个数组的内存地址
print("零数组的内存地址:", id(zeros_array))
print("未初始化数组的内存地址:", id(empty_array))
# 验证两个数组的相同元素是否共享内存
a = zeros_array[0, 0]
b = empty_array[0, 0]
print("零数组的首元素内存地址:", id(a))
print("未初始化数组的首元素内存地址:", id(b))
注释:
# 导入 NumPy 库,并将其别名为 np
import numpy as np
# 创建一个 3x3 的零数组
# np.zeros 是 NumPy 中用于创建全零数组的函数
# 传入数组的形状和数据类型作为参数
zeros_array = np.zeros((3, 3), dtype=np.float64)
print("零数组:")  # 打印零数组
print(zeros_array)
# 创建一个 3x3 的未初始化数组
# np.empty 是 NumPy 中用于创建未初始化数组的函数
# 传入数组的形状和数据类型作为参数
empty_array = np.empty((3, 3), dtype=np.float64)
print("未初始化数组:")  # 打印未初始化数组
print(empty_array)
# 验证两个数组的内存地址
# id() 函数用于获取对象的内存地址
print("零数组的内存地址:", id(zeros_array))
print("未初始化数组的内存地址:", id(empty_array))
# 验证两个数组的相同元素是否共享内存
# 获取零数组和未初始化数组的首元素
a = zeros_array[0, 0]
b = empty_array[0, 0]
print("零数组的首元素内存地址:", id(a))
print("未初始化数组的首元素内存地址:", id(b))
1.2.5 数组元属性操作实验(shape修改的边界条件)
ndarray 的 shape 属性可以动态修改,但有一些边界条件需要遵守。我们将通过实验验证这些边界条件。
import numpy as np
# 创建一个 3x3 的数组
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float64)
print("原始数组:")
print(array)
# 修改数组的形状为 1x9
array.shape = (1, 9)
print("修改后的数组(1x9):")
print(array)
# 修改数组的形状为 9x1
array.shape = (9, 1)
print("修改后的数组(9x1):")
print(array)
# 尝试修改数组的形状为 4x3
try:
    array.shape = (4, 3)
except ValueError as e:
    print("尝试修改形状为 4x3 时的错误:", e)
# 尝试修改数组的形状为 3x3x3
try:
    array.shape = (3, 3, 3)
except ValueError as e:
    print("尝试修改形状为 3x3x3 时的错误:", e)
# 修改数组的形状为 3x3
array.shape = (3, 3)
print("恢复数组形状为 3x3:")
print(array)
注释:
# 导入 NumPy 库,并将其别名为 np
import numpy as np
# 创建一个 3x3 的数组
# np.array 是 NumPy 中用于创建数组的函数
# 传入二维列表,每个子列表代表数组的一行,指定数据类型为 64 位浮点数
array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float64)
print("原始数组:")  # 打印原始数组
print(array)
# 修改数组的形状为 1x9
# .shape 属性用于获取或设置数组的形状
array.shape = (1, 9)
print("修改后的数组(1x9):")  # 打印修改后的数组
print(array)
# 修改数组的形状为 9x1
array.shape = (9, 1)
print("修改后的数组(9x1):")  # 打印修改后的数组
print(array)
# 尝试修改数组的形状为 4x3
# 这将导致 ValueError,因为数组的总元素数(9)不等于目标形状的总元素数(12)
try:
    array.shape = (4, 3)
except ValueError as e:
    print("尝试修改形状为 4x3 时的错误:", e)
# 尝试修改数组的形状为 3x3x3
# 这将导致 ValueError,因为数组的总元素数(9)不等于目标形状的总元素数(27)
try:
    array.shape = (3, 3, 3)
except ValueError as e:
    print("尝试修改形状为 3x3x3 时的错误:", e)
# 修改数组的形状为 3x3
# 成功修改回原形状
array.shape = (3, 3)
print("恢复数组形状为 3x3:")  # 打印恢复后的数组
print(array)
总结
通过这篇文章,我们深入解析了 NumPy 的 ndarray 的底层实现,探讨了其内存结构、维度、数据类型、步长等关键概念,并通过实验验证了这些概念的实际应用。希望这些内容能帮助你更好地理解和使用 NumPy。
参考文献或资料
| 参考资料名称 | 链接 | 
|---|---|
| NumPy 官方文档 | https://numpy.org/doc/ | 
| Python 官方文档 | https://docs.python.org/3/ | 
| NumPy 入门指南 | https://numpy.org/devdocs/user/quickstart.html | 
| NumPy 源码分析 | https://github.com/numpy/numpy | 
| NumPy 速查表 | https://www.kaggle.com/learn/overview | 
| NumPy 实战案例 | https://www.tensorflow.org/tutorials/quickstart/beginner | 
| NumPy 书籍推荐 | https://www.springer.com/gp/book/9781484242452 | 
| NumPy 视频教程 | https://www.youtube.com/watch?v=QUT1VHiLmmI | 
| NumPy 交互式学习 | https://colab.research.google.com/ | 
| Python 内存管理 | https://docs.python.org/3/c-api/memory.html | 
| C 语言内存管理 | https://en.wikipedia.org/wiki/C_memory_allocation | 
| 数据结构与算法 | https://www.geeksforgeeks.org/ | 
| 深度学习中的数组操作 | https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html | 
| 科学计算库对比 | https://www.tensorflow.org/compare | 
| 高效计算技术 | https://en.wikipedia.org/wiki/High-performance_computing | 
| 编程社区讨论 | https://stackoverflow.com/questions/tagged/numpy | 
希望这篇文章能帮助你在 NumPy 的学习和使用中更进一步。如果有任何问题或需要进一步的帮助,欢迎留言讨论!


















