【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.2 多维数组切片:跨步访问与内存布局

在这里插入图片描述

2.2 多维数组切片:跨步访问与内存布局

目录/提纲
Syntax error in textmermaid version 10.9.0
2.2.1 跨步(Strides)内存模型
  2.2.1.1 跨步概念
  2.2.1.2 跨步内存模型
  2.2.1.3 跨步的计算方法
2.2.2 负步长切片技巧
  2.2.2.1 负步长的概念
  2.2.2.2 负步长的应用
  2.2.2.3 负步长的内存布局
2.2.3 分块切片优化
  2.2.3.1 分块的概念
  2.2.3.2 分块切片的方法
  2.2.3.3 分块切片的性能优势
2.2.4 大数组切片性能陷阱
  2.2.4.1 内存复制问题
  2.2.4.2 视图 vs 复制
  2.2.4.3 优化策略
Syntax error in textmermaid version 10.9.0
文章内容

NumPy 的多维数组切片是处理数组时非常强大的工具,它使得我们可以灵活地访问和操作数组中的子数组。本文将详细介绍多维数组切片的跨步访问机制、负步长切片技巧以及分块切片优化方法。通过本文的学习,读者将能够更好地理解 NumPy 的多维数组切片原理,并在实际应用中更加高效地使用这些功能。

2.2.1 跨步(Strides)内存模型

2.2.1.1 跨步概念

跨步(Strides)是 NumPy 数组的一个重要属性,它描述了每个维度上的元素在内存中的间隔。通过跨步,NumPy 可以高效地访问数组中的元素,而不需要重新分配内存。

原理说明
  • 跨步的定义:跨步是指从当前元素到下一元素在内存中的字节数偏移量。对于多维数组,每个维度都有一个跨步值。
  • 跨步的作用:跨步使得 NumPy 可以通过简单的指针操作来访问数组中的元素,而不需要重新计算每个元素的位置。
NumPy数组的跨步访问由strides属性控制,其数学定义:

strides [ i ] = itemsize × ∏ j = i + 1 d − 1 shape [ j ] \text{strides}[i] = \text{itemsize} \times \prod_{j=i+1}^{d-1} \text{shape}[j] strides[i]=itemsize×j=i+1d1shape[j]

其中d是数组维度,itemsize是元素字节大小。以三维数组(2,3,4)为例:

arr = np.arange(24).reshape(2,3,4)
print(arr.strides)  # (48, 16, 4)  # 每个维度的字节步长

内存布局示意图(Mermaid):

Syntax error in textmermaid version 10.9.0
示例代码
import numpy as np

# 创建一个 3x3 的 NumPy 数组
arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])
# 获取数组的跨步信息
strides = arr.strides
print(strides)  # 输出 (24, 8) 说明第0维(行)的步长为24字节,第1维(列)的步长为8字节
2.2.1.2 跨步内存模型

跨步内存模型是 NumPy 数组在内存中的存储方式。NumPy 数组默认以 C 顺序存储,即行优先。这意味着同一行中的元素在内存中是连续的,而不同行之间的元素则有固定的间隔。

内存布局示意图
graph TD
  A[NumPy 数组]
  A --> B[第0维(行)]
  A --> C[第1维(列)]
  B --> D[步长: 24字节]
  C --> E[步长: 8字节]
  F[内存布局]
  F --> G[1, 2, 3]
  F --> H[4, 5, 6]
  F --> I[7, 8, 9]
2.2.1.3 跨步的计算方法

跨步的计算方法简单直观。对于一个 ((n, m)) 形状的数组,如果每个元素占用 itemsize 字节,那么第0维(行)的跨步为 (m \times \text{itemsize}),第1维(列)的跨步为 (\text{itemsize})。

计算公式

[
\text{strides}_0 = m \times \text{itemsize}
]
[
\text{strides}_1 = \text{itemsize}
]

示例代码
# 创建一个 3x3 的 NumPy 数组
arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]], dtype=np.int32)
# 获取数组的形状和每个元素的字节大小
shape = arr.shape
itemsize = arr.itemsize

# 计算跨步
strides_0 = shape[1] * itemsize
strides_1 = itemsize

print(f"第0维的步长: {strides_0} 字节")  # 输出 12 字节
print(f"第1维的步长: {strides_1} 字节")  # 输出 4 字节

2.2.2 负步长切片技巧

2.2.2.1 负步长的概念

负步长切片允许我们以相反的顺序访问数组中的元素。这在某些情况下非常有用,例如反转数组、提取特定的子数组等。

原理说明
  • 负步长的使用:负步长可以通过指定步长为负数来实现。例如,arr[::-1] 会反转数组。
  • 内存布局:负步长切片操作不会立即复制内存,而是返回一个视图。这意味着原数组中的元素仍然按原来的顺序存储,只是访问时按照相反的顺序。

内存访问示意图:

Syntax error in textmermaid version 10.9.0
示例代码
# 创建一个 NumPy 数组
arr = np.array([1, 2, 3, 4, 5])
# 反转数组
reversed_arr = arr[::-1]
print(reversed_arr)  # 输出 [5 4 3 2 1]

# 逆序提取子数组
sub_arr = arr[-1:1:-1]
print(sub_arr)  # 输出 [5 4 3 2]
2.2.2.2 负步长的应用

负步长切片在逆序操作、提取子数组等方面有广泛的应用。

逆序操作
# 创建一个 3x3 的 NumPy 数组
arr = np.array([[1, 2, 3],
                [4, 5, 6],
                [7, 8, 9]])
# 反转数组
reversed_arr = arr[::-1, ::-1]
print(reversed_arr)  # 输出 [[9 8 7]
                     #        [6 5 4]
                     #        [3 2 1]]
提取子数组
# 创建一个 10x10 的 NumPy 数组
arr = np.arange(100).reshape(10, 10)
# 逆序提取子数组
sub_arr = arr[-2::-2, -2::-2]
print(sub_arr)  # 输出 [[88 86 84 82 80]
                #        [68 66 64 62 60]
                #        [48 46 44 42 40]
                #        [28 26 24 22 20]
                #        [ 8  6  4  2  0]]
2.2.2.3 负步长的内存布局

负步长切片操作返回的是原数组的一个视图,这意味着内存布局不会改变,只是访问顺序不同。

内存布局示意图
Syntax error in textmermaid version 10.9.0

2.2.3 分块切片优化

2.2.3.1 分块的概念

分块(Chunking)是指将大数组划分为多个小数组进行处理。分块可以减少内存使用,提高计算效率,特别是在处理大数组时。

原理说明
  • 分块的目的:通过分块,我们可以避免一次性加载整个大数组到内存中,从而减少内存开销。
  • 分块的方法:可以使用 np.array_splitnp.split 等函数将数组分块。
示例代码
# 创建一个大型 NumPy 数组
arr = np.arange(100)  # 生成 0 到 99 的数组
# 分块
chunks = np.array_split(arr, 10)  # 将数组分为 10 个块

# 打印每个分块
for i, chunk in enumerate(chunks):
    print(f"分块 {i}: {chunk}")
2.2.3.2 分块切片的方法

分块切片可以通过多种方法实现,包括 np.array_splitnp.split 以及手动分块。

方法说明
  • np.array_split:将数组沿指定轴均匀分块,但允许最后一块稍小一些。
  • np.split:将数组沿指定轴均匀分块,要求每块大小相同。
  • 手动分块:通过手动计算索引范围来分块。
示例代码
# 创建一个 100x100 的大型 NumPy 数组
arr = np.arange(10000).reshape(100, 100)  # 生成 0 到 9999 的数组,并 reshape 为 100x100
# 使用 np.array_split 沿第0轴分块
row_chunks = np.array_split(arr, 10)
# 使用 np.split 沿第1轴分块
col_chunks = []
for chunk in row_chunks:
    col_chunks.append(np.split(chunk, 5, axis=1))

# 打印每个分块
for i, chunk in enumerate(row_chunks):
    print(f"行分块 {i}: {chunk}")

for i, chunk in enumerate(col_chunks):
    for j, sub_chunk in enumerate(chunk):
        print(f"列分块 {i}-{j}: {sub_chunk}")
2.2.3.3 分块切片的性能优势

分块切片可以显著提高处理大数组的性能,减少内存占用,避免缓存缺失等问题。

性能测试代码
import time

# 创建一个 10000x10000 的大型 NumPy 数组
arr = np.arange(100000000).reshape(10000, 10000)

# 测试直接处理大数组
start_time = time.time()
result_direct = arr[1000:2000, 5000:6000]
end_time = time.time()
time_direct = end_time - start_time

# 测试分块处理
start_time = time.time()
row_chunks = np.split(arr, 100, axis=0)
col_chunks = []
for chunk in row_chunks:
    col_chunks.append(np.split(chunk, 50, axis=1))

result_chunked = col_chunks[10][25]
end_time = time.time()
time_chunked = end_time - start_time

print(f"直接处理耗时: {time_direct:.6f} 秒")
print(f"分块处理耗时: {time_chunked:.6f} 秒")

2.2.4 大数组切片性能陷阱

2.2.4.1 内存复制问题

在处理大数组时,切片操作可能会导致内存复制问题。如果返回的是数组的复制,而不仅仅是视图,那么会显著增加内存开销。

示例代码
# 创建一个大型 NumPy 数组
arr = np.arange(10000000)

# 测试直接切片
start_time = time.time()
result_direct = arr[10000:20000]
end_time = time.time()
time_direct = end_time - start_time

# 测试使用视图切片
start_time = time.time()
result_view = arr[10000:20000]
end_time = time.time()
time_view = end_time - start_time

# 测试使用复制切片
start_time = time.time()
result_copy = arr[10000:20000].copy()
end_time = time.time()
time_copy = end_time - start_time

print(f"直接切片耗时: {time_direct:.6f} 秒")
print(f"视图切片耗时: {time_view:.6f} 秒")
print(f"复制切片耗时: {time_copy:.6f} 秒")
2.2.4.2 视图 vs 复制

理解 NumPy 切片返回的是视图还是复制非常重要。视图不复制数据,而复制会创建新的数据副本。

示例代码
# 创建一个 NumPy 数组
arr = np.array([1, 2, 3, 4, 5])

# 视图切片
view = arr[1:4]
print(view)  # 输出 [2 3 4]

# 修改原数组
arr[2] = 10
print(view)  # 输出 [2 10 4] 视图也改变了

# 复制切片
copy = arr[1:4].copy()
print(copy)  # 输出 [2 10 4]

# 修改原数组
arr[2] = 20
print(copy)  # 输出 [2 10 4] 复制没有改变
2.2.4.3 优化策略

为了提高大数组切片的性能,我们可以采用以下策略:

  • 使用视图:尽量避免使用 copy 方法,除非确实需要独立的数据副本。
  • 分块处理:将大数组分块,逐块处理,减少内存占用。
  • 避免不必要的切片:尽量减少对同一个数组进行多次切片操作,可以将多次操作合并为一次。
优化示例
# 创建一个大型 NumPy 数组
arr = np.arange(10000000)

# 使用视图切片
start_time = time.time()
result_view = arr[10000:20000]
end_time = time.time()
time_view = end_time - start_time

# 使用分块处理
start_time = time.time()
row_chunks = np.split(arr, 1000)
result_chunked = row_chunks[10]
end_time = time.time()
time_chunked = end_time - start_time

# 使用 avoidance 切片
start_time = time.time()
result_avoidance = arr[10000:20000]
end_time = time.time()
time_avoidance = end_time - start_time

print(f"视图切片耗时: {time_view:.6f} 秒")
print(f"分块处理耗时: {time_chunked:.6f} 秒")
print(f"避免不必要的切片耗时: {time_avoidance:.6f} 秒")

总结

通过本文的学习,读者将能够更好地理解 NumPy 多维数组切片的跨步访问机制、负步长切片技巧以及分块切片优化方法。跨步内存模型使得 NumPy 可以高效地访问数组中的元素,负步长切片提供了灵活的逆序访问方式,而分块切片则可以帮助我们在处理大数组时减少内存占用,提高计算效率。希望本文的内容能够帮助读者在实际应用中更好地利用这些高级功能。

参考资料


这篇文章包含了详细的原理介绍、代码示例、源码注释以及案例等。希望这对您有帮助。如果有任何问题请随私信或评论告诉我。

posted @   爱上编程技术  阅读(7)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示