一维数组切片:基础语法与边界行为
切片,说白了就是从一个数组里「切」出一段来。Python 的切片语法 [start:stop:step] 看起来简单,但我在实际项目中见过太多人在这上面翻车。今天咱们就把这个基础但容易踩坑的东西彻底讲透。
基础语法:三个参数的含义
先看一个最直观的例子:
import numpy as np
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 从索引2开始,到索引7结束(不包含7),步长2
slice1 = arr[2:7:2]
print(slice1) # 输出:[2 4 6]
三个参数的作用很清晰:
- start:起始索引(包含)
- stop:结束索引(不包含)
- step:步长(默认为1)
嗯,这里要注意一个关键点:stop 是不包含的。这是新手最容易搞混的地方。我刚开始学的时候也纳闷过:「我要取到第7个元素,为什么写7却取不到?」后来才明白,Python 的设计哲学是「左闭右开」——左边包含,右边不包含。
省略参数的几种写法
实际开发中,我们经常省略某些参数。我个人习惯这样用:
| 写法 | 含义 | 示例 | 结果 |
|---|---|---|---|
arr[:5] |
从开头到索引5(不包含) | arr[:5] |
[0 1 2 3 4] |
arr[3:] |
从索引3到末尾 | arr[3:] |
[3 4 5 6 7 8 9] |
arr[:] |
整个数组的副本 | arr[:] |
[0 1 2 3 4 5 6 7 8 9] |
arr[::2] |
从头到尾,步长2 | arr[::2] |
[0 2 4 6 8] |
arr[::-1] |
反转数组 | arr[::-1] |
[9 8 7 6 5 4 3 2 1 0] |
你想想看,arr[::-1] 这个写法多优雅。一个切片就把数组反转了,不用写循环,不用调函数。这就是 Python 切片设计的精妙之处。
边界行为:超出索引会怎样?
很多语言里,数组索引越界会直接报错。但 Python 切片不一样——它很宽容。我曾在项目中用切片处理动态长度的数据流,就是因为这个特性省了不少边界判断的代码。
arr = np.array([0, 1, 2, 3, 4])
# start 超出范围
print(arr[10:]) # 输出:[]
print(arr[10:20]) # 输出:[]
# stop 超出范围
print(arr[:10]) # 输出:[0 1 2 3 4]
print(arr[2:10]) # 输出:[2 3 4]
# 负数索引
print(arr[-3:]) # 输出:[2 3 4]
print(arr[:-2]) # 输出:[0 1 2]
核心规则:切片不会因为索引越界而抛出异常。start 超出范围返回空数组,stop 超出范围自动截断到末尾。负数索引从数组末尾开始计数,-1 表示最后一个元素。
常见陷阱:你以为的未必是你以为的
好了,基础讲完了。下面这些坑,我一个个都踩过,你最好也记住。
陷阱一:切片是视图,不是副本
这是 NumPy 切片和 Python 列表切片最大的区别。NumPy 切片返回的是原数组的视图,修改视图会影响原数组。
arr = np.array([0, 1, 2, 3, 4, 5])
slice_view = arr[2:5]
slice_view[0] = 99
print(arr) # 输出:[ 0 1 99 3 4 5] —— 原数组被改了!
我曾经在数据处理流水线里犯过这个错。一个模块切了数据去处理,结果把原始数据污染了,排查了半天才发现是视图引用的问题。如果你需要独立的数据副本,记得用 .copy():
arr = np.array([0, 1, 2, 3, 4, 5])
slice_copy = arr[2:5].copy()
slice_copy[0] = 99
print(arr) # 输出:[0 1 2 3 4 5] —— 原数组没变
陷阱二:步长为负数时的 start 和 stop
步长为负数时,切片是从右向左取的。这时候 start 和 stop 的位置关系要反过来理解。
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 从索引8开始向左取到索引3(不包含3)
print(arr[8:3:-1]) # 输出:[8 7 6 5 4]
# 从末尾开始向左取到开头
print(arr[::-1]) # 输出:[9 8 7 6 5 4 3 2 1 0]
为什么会这样?因为步长为负时,start 是起始点,stop 是终止点,但方向是反的。我建议你记住一个口诀:「步长正,从左到右;步长负,从右到左」。
陷阱三:start 和 stop 同时省略时,步长正负的区别
arr = np.array([0, 1, 2, 3, 4, 5])
# 步长为正:从头到尾
print(arr[::1]) # 输出:[0 1 2 3 4 5]
# 步长为负:从尾到头
print(arr[::-1]) # 输出:[5 4 3 2 1 0]
这个看起来简单,但结合负数索引就容易出问题。比如 arr[-1::-1] 和 arr[::-1] 结果一样,但写法更啰嗦。我个人习惯直接用 arr[::-1],简洁明了。
陷阱四:空步长与步长为1的区别
步长默认为1,但如果你显式写 step=None,效果是一样的。不过有一种情况要注意:
arr = np.array([0, 1, 2, 3, 4, 5])
# 这两种写法等价
print(arr[1:4]) # 输出:[1 2 3]
print(arr[1:4:1]) # 输出:[1 2 3]
# 但步长为0会报错
# print(arr[1:4:0]) # ValueError: slice step cannot be zero
注意:步长不能为0,否则会抛出 ValueError。这在动态生成切片参数时尤其要小心,我曾经在配置文件中写错过步长参数,导致整个批处理任务崩溃。
知识体系总览
下面这张图把一维数组切片的核心知识点串起来了,你可以对照着回顾一下:
避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的切片问题,希望能帮你少走弯路。
我曾经在写数据预处理脚本时,用切片取训练集的前1000条数据。代码跑了一周,结果发现因为视图引用,验证集的数据被训练过程污染了。从那以后,只要涉及数据分割,我必加 .copy()。
还有一次,我在处理时间序列数据时,想取最后10个时间步。写了 data[-10:],结果数据长度只有5,返回了全部5条。幸好下游逻辑能处理空数据,不然就崩了。所以,动态切片时最好加个长度判断。
另外,用 arr[::-1] 反转数组虽然方便,但如果数组很大(比如上亿元素),它会创建一个新的视图,不会复制数据。如果你后续要修改反转后的数组,记得用 .copy(),否则原数组也会被改。