索引、切片与拷贝
本章目录
本章学习目标
- 区分基本切片与高级索引的内存语义
- 正确按条件写回原数组
- 检查 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。
记录尽量保存在当前浏览器,建议定期导出备份。