跳到正文
本地阅读 · 索引、切片与拷贝

索引、切片与拷贝

计算机基础 · NumPy

前置知识:ndarray、shape 与 dtype

本章目录

本章学习目标

  • 区分基本切片与高级索引的内存语义
  • 正确按条件写回原数组
  • 检查 reshape、转置和共享内存

一、定位元素与保留形状

对两行三列 a=[[0,1,2],[3,4,5]],a[1,2] 取值为五的元素,即第二行第三列;a[:,1] 取第二列,结果是一维 [1,4];a[:,1:2] 同样取这一列但保留二维形状 (2,1)。冒号表示该轴全部,负索引从末尾计数,半开切片包含起点不含终点,与 Python 序列一致。

维度变化会影响后续广播,不要只看选出的数字相同就认定表达式等价。整数索引通常消去被选轴,长度一的切片通常保留轴。按行取第零和第一行可用 a[[0,1],:],按两组索引 a[[0,1],[1,2]] 则取两个配对位置,不是两行两列笛卡尔子矩阵;想要后者可使用 np.ix_。先画位置,再预测形状,能避免许多无异常的选择错误。

二、基本切片共享原数据

基本切片通常得到 view,改变形状或 strides 来访问同一缓冲区。v=a[:,:2] 后执行 v[0,0]=99,原数组 a 的左上角也变为九十九。视图并不等于只读,是否可写取决于数组标志和来源。若需要保留原始数据,显式 v=a[:,:2].copy(),再修改 v;这一步付出内存成本换来独立性。

一个很小的视图也可能使大的原缓冲区继续占用内存,因为它仍引用原数据。将长期保留的小切片复制出来,有时比维持视图更合适。不要单凭 .base is a 判断全部共享关系,因为视图链和 reshape 可以让 base 指向更早的对象;用 np.shares_memory 检查具体两数组是否共享数据,同时通过小型修改反例验证预期。

三、高级索引复制,但赋值能写回

整数数组索引和布尔索引属于高级索引,作为取值操作通常返回 copy。例如 b=a[a>2] 取出 [3,4,5],修改 b 不影响 a。但 a[a>2]=-1 是直接对原数组进行索引赋值,会把这些位置写为负一。两者区别不在布尔条件,而在你是把选择结果取出再改,还是把赋值目标直接写在原数组索引表达式上。

反例 a[a>2][0]=100 通常只修改临时拷贝,原数据保持不变,这类链式索引可能不报错却没有完成任务。推荐一次性构造 mask,再 a[mask]=value。掩码 shape 必须匹配被选择轴,不能把样本掩码直接当特征掩码。使用 &、|、~ 组合数组条件,并用括号包住比较;Python 的 and/or 需要单一真值,不适用于多元素数组。

四、reshape 不保证永远是视图

连续数组 reshape 经常无需复制,但非连续数组、转置结果或特殊 strides 可能需要复制才能满足新布局。a.T 通常交换形状和步幅,共享数据;将其按某种顺序展平成新形状时是否复制,要检查实际结果,不能背诵“reshape 一定不复制”。ravel 尽量返回一维视图,flatten 返回拷贝;同样应该基于接口文档和共享检查作判断。

若核心算法要求独立可写输入,入口显式 copy 比依赖偶然连续性更可靠;若要求性能,则测量额外分配并说明允许共享的条件。NumPy 数组的 shape 与存储布局是不同信息:形状相同的两个数组可能一个连续、另一个跨步访问,底层库调用的成本也可能不同。不要为了修复一个维度错误就随意 reshape,把原本日期优先的数据解释成股票优先。

五、用观察实验建立内存直觉

配套代码依次演示基本切片修改、整数高级索引修改、布尔直接赋值和转置后的 reshape。每一步用新数组或明确记录变化,避免前一个实验污染下一个。共享关系打印出来,绝不根据变量名推断。教学交互用固定数组模拟这些修改,真实视图和内存行为仍以 Python 输出为准。

在数据项目中先约定输入能否被改,尤其用于多组模型共享特征时,原地标准化可能影响后续实验。加入空掩码时,直接赋值应保持数据不变;加入全真掩码时,全部元素被修改。能够解释这两个边界和链式索引的临时对象,才算理解“选中数据”与“更新数据”的区别。学习这个机制也有助于理解 pandas 3.x 的 Copy-on-Write,但不要假定两套对象完全相同。

交互演示:观察机制

完整示例与实际输出

import numpy as np
a=np.arange(6).reshape(2,3)
view=a[:,:2]; view[0,0]=99
assert a[0,0]==99 and np.shares_memory(a,view)
copied=a[[0,1],:]; copied[0,0]=-9
assert a[0,0]==99 and not np.shares_memory(a,copied)
print('after view change',a.tolist())
a=np.arange(6).reshape(2,3)
selected=a[a>2]; selected[:]=-1
assert a.tolist()==[[0,1,2],[3,4,5]]
a[a>2]=-1
assert a.tolist()==[[0,1,2],[-1,-1,-1]]
print('direct mask assignment',a.tolist())
x=np.arange(12).reshape(3,4); t=x.T; r=t.reshape(2,6)
assert np.shares_memory(x,t)
assert r.shape==(2,6) and sorted(r.ravel().tolist())==list(range(12))
print('transpose shares',np.shares_memory(x,t),'reshape shares',np.shares_memory(x,r))

实际运行通过CPU Python execution.

after view change [[99, 1, 2], [3, 4, 5]]
direct mask assignment [[0, 1, 2], [-1, -1, -1]]
transpose shares True reshape shares False

五道自测:先回答,再核对

01 · a[:,1] 与 a[:,1:2] 的维度区别?

查看提示

整数索引与切片。

查看参考答案

前者通常为一维,后者保留第二轴长度一。

02 · 基本切片 v 修改后为何影响 a?

查看提示

缓冲区共享。

查看参考答案

通常是视图,二者访问同一数据。

03 · b=a[a>0] 后修改 b 是否改 a?

查看提示

高级索引取值。

查看参考答案

通常不改,布尔取值返回拷贝。

04 · a[a>0]=0 是否会写回?

查看提示

赋值目标是哪一个对象?

查看参考答案

会,直接索引赋值写入 a 选中的位置。

05 · reshape 一定不复制吗?

查看提示

存储布局可能不兼容。

查看参考答案

不保证;需要检查共享内存或按接口明确要求独立拷贝。

两项动手任务

任务 1 · 选出 [[1,-2],[3,-4]] 中负数并写为零,保持形状。

提示

对原数组一次索引赋值。

完整参考解答与检查方式

不要先取出负数副本再修改。

import numpy as np
a=np.array([[1,-2],[3,-4]])
mask=a<0; a[mask]=0
assert a.tolist()==[[1,0],[3,0]]
print(a.tolist())

任务 2 · 用同一原数组演示基本切片共享、copy 独立和整数高级索引独立。

提示

shares_memory 配合修改检查。

完整参考解答与检查方式

显式复制和高级索引取值不会改变原数组;基本切片会。

import numpy as np
a=np.arange(5); view=a[1:3]; own=view.copy(); fancy=a[[1,2]]
own[0]=20; fancy[0]=30
assert a[1]==1
view[0]=10
assert a[1]==10 and own[0]==20 and fancy[0]==30
assert np.shares_memory(a,view) and not np.shares_memory(a,own)
print(a.tolist(),own.tolist(),fancy.tolist())

官方资料与掌握状态

完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。

动手任务记录

掌握状态与复习

关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。

记录尽量保存在当前浏览器,建议定期导出备份。