← 返回Python进阶编程目录
← 返回学习笔记首页
专题: Python进阶编程系统学习
关键词: Python, 切片, slice, __getitem__, 切片协议, Ellipsis, 多维切片, 自定义切片
一、概述
Python切片(Slicing)是一种从序列类型中提取子序列的强大机制。它通过简洁的 start:stop:step 语法,允许开发者快速获取列表、元组、字符串等序列中的部分元素。然而,切片的真正威力远不止于此——在Python的底层,切片背后是 slice 对象和 __getitem__ 协议的协作。深入理解这一机制,可以让开发者为自定义类实现优雅的切片接口,甚至构建出像NumPy数组那样支持多维切片的复杂数据结构。
Python的切片设计语言体现了"约定优于配置"的思想。看似简单的 lst[1:10:2] 语法,实际上Python解释器会在编译时将其转换为 lst.__getitem__(slice(1, 10, 2)) 调用。这种设计将语法糖与底层协议分离,使开发者可以在自定义类型中完全控制切片行为。
本文将从切片的基本语法出发,逐步深入到slice对象、indices方法、自定义切片类、多维切片(元组索引)、Ellipsis省略号,以及在数据框和矩阵中的高级应用,帮助读者建立起完整的切片知识体系。
二、切片基本语法:start:stop:step
切片语法是Python最常用的特性之一。其完整形式为 seq[start:stop:step],三个参数的含义如下:
start: 切片起始索引(包含),默认为0(正步进)或len-1(负步进)
stop: 切片结束索引(不包含),默认为序列长度(正步进)或-1(负步进)
step: 步进值,默认为1;可为负数实现反向切片
基本用法示例
# 基本列表切片
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 基本形式:取索引 2 到 5(不包含5)
print(nums[2:5]) # [2, 3, 4]
# 省略start:从开头开始
print(nums[:5]) # [0, 1, 2, 3, 4]
# 省略stop:直到末尾
print(nums[5:]) # [5, 6, 7, 8, 9]
# 省略所有:完整副本
print(nums[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 指定步进:每隔一个取一个
print(nums[::2]) # [0, 2, 4, 6, 8]
# 负步进:反向切片
print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
# 负步进取子集
print(nums[7:2:-1]) # [7, 6, 5, 4, 3]
负索引的边界行为
Python支持负索引,从序列末尾开始计数,-1表示最后一个元素。当负索引与切片结合使用时,需要特别注意边界行为:
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 负索引切片
print(nums[-5:-1]) # [5, 6, 7, 8] (从倒数第5到倒数第1,不包含)
print(nums[-5:]) # [5, 6, 7, 8, 9] (从倒数第5到末尾)
print(nums[:-3]) # [0, 1, 2, 3, 4, 5, 6] (从开头到倒数第3,不包含)
# 负索引 + 负步进
print(nums[-1:-5:-1]) # [9, 8, 7, 6] (从末尾往前取)
# 超出边界的处理(Python自动截断)
print(nums[-100:5]) # [0, 1, 2, 3, 4] (start自动截断为0)
print(nums[5:100]) # [5, 6, 7, 8, 9] (stop自动截断为len)
边界规则: Python的切片会自动处理越界索引——当start或stop超出序列范围时,不会抛出IndexError,而是被自动截断到有效范围内。这一特性使切片代码更加健壮。
字符串和元组切片
切片不仅适用于列表,同样适用于字符串和元组等所有序列类型:
# 字符串切片
text = "Python切片机制"
print(text[0:6]) # Python
print(text[::-1]) # 制机片切nohtyP
print(text[::2]) # Pto切机制
# 元组切片
t = (1, 2, 3, 4, 5)
print(t[1:4]) # (2, 3, 4)
print(t[::-1]) # (5, 4, 3, 2, 1)
性能提示: 列表切片会创建新列表(浅拷贝),时间复杂度O(k),其中k为切片长度。对于大列表的频繁切片操作,可以考虑使用itertools.islice进行惰性求值。
三、slice对象与indices方法
当我们写 obj[1:10:2] 时,Python解释器会创建一个 slice(1, 10, 2) 对象,然后调用 obj.__getitem__(slice(1, 10, 2))。这意味着切片本质上就是对slice对象的操作。slice对象是切片语法的底层表示,理解它对于自定义切片行为至关重要。
slice对象的创建和属性
# 显式创建slice对象
s1 = slice(5) # 相当于 [:5]
s2 = slice(2, 8) # 相当于 [2:8]
s3 = slice(1, 9, 2) # 相当于 [1:9:2]
print(s1.start, s1.stop, s1.step) # None 5 None
print(s2.start, s2.stop, s2.step) # 2 8 None
print(s3.start, s3.stop, s3.step) # 1 9 2
# slice对象的字符串表示
print(repr(s3)) # slice(1, 9, 2)
indices方法:计算实际索引
slice.indices(length) 方法是理解切片行为的关键。它接收序列的长度作为参数,返回一个三元组 (start, stop, step),表示在给定长度的序列上,该切片实际映射到的索引范围。所有的负索引和边界截断都在这个方法中得到处理:
# indices 方法:将切片映射到具体索引
s = slice(-5, -1, 1) # 相当于 [-5:-1:1]
print(s.indices(10)) # (5, 9, 1)
s2 = slice(-1, -5, -1) # 相当于 [-1:-5:-1]
print(s2.indices(10)) # (9, 5, -1)
# 超出边界的切片
s3 = slice(-100, 100)
print(s3.indices(10)) # (0, 10, 1) ——start被截断为0,stop截断为10
# 步进为负时的边界
s4 = slice(100, -100, -1)
print(s4.indices(10)) # (9, -1, -1) ——start截断为9,stop截断为-1
理解indices方法的返回值至关重要。正步进时,start和stop都被截断到 [0, length] 区间;负步进时,start截断为 length - 1,stop截断为 -1。这使得我们在实现自定义切片时,可以统一使用indices方法来计算实际遍历范围,而无需自行处理各种边界情况。
手动模拟切片行为
def manual_slice(seq, s):
"""手动模拟切片行为"""
length = len(seq)
start, stop, step = s.indices(length)
result = []
i = start
while (step > 0 and i < stop) or (step < 0 and i > stop):
result.append(seq[i])
i += step
return result
# 测试
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(manual_slice(nums, slice(2, 8, 2))) # [2, 4, 6]
print(manual_slice(nums, slice(-5, -1))) # [5, 6, 7, 8]
print(manual_slice(nums, slice(None, None, -1))) # [9,8,7,6,5,4,3,2,1,0]
核心理解: indices方法是连接切片语法和实际数据访问的桥梁。无论切片表达式中使用的是正索引、负索引还是省略形式,indices方法总能计算出统一的、与序列长度适配的实际索引范围。自定义切片类时,务必善用这个方法。
四、__getitem__接收slice参数实现自定义切片
Python的切片协议核心就是 __getitem__ 方法。当我们写 obj[key] 时,Python会根据 key 的类型(整数、slice、元组等)将其传递给 __getitem__。如果希望自定义类支持切片,就需要在 __getitem__ 中处理slice类型的参数。
基础知识:__getitem__的单键和切片分发
class SimpleList:
def __init__(self, data):
self.data = list(data)
def __getitem__(self, key):
if isinstance(key, slice):
# 处理切片访问
start, stop, step = key.indices(len(self.data))
return [self.data[i] for i in range(start, stop, step)]
elif isinstance(key, int):
# 处理整数索引
return self.data[key]
else:
raise TypeError(f"不支持的索引类型: {type(key)}")
# 测试
sl = SimpleList(range(10))
print(sl[2]) # 2 (整数索引)
print(sl[2:7:2]) # [2, 4, 6] (切片)
print(sl[:5]) # [0, 1, 2, 3, 4]
支持切片赋值:__setitem__
仅实现 __getitem__ 只能实现读取切片。要实现切片赋值(如 obj[1:3] = [10, 20]),还需要实现 __setitem__:
class MutableList:
def __init__(self, data=None):
self.data = list(data) if data else []
def __getitem__(self, key):
if isinstance(key, slice):
start, stop, step = key.indices(len(self.data))
if step != 1:
# 步进不为1时,返回对应元素列表
return [self.data[i] for i in range(start, stop, step)]
return self.data[start:stop]
elif isinstance(key, int):
return self.data[key]
raise TypeError(f"不支持的索引类型: {type(key)}")
def __setitem__(self, key, value):
if isinstance(key, slice):
start, stop, step = key.indices(len(self.data))
if step != 1:
# 步进不为1时,逐元素赋值
indices = list(range(start, stop, step))
if len(indices) != len(value):
raise ValueError(
f"切片元素数量({len(indices)})与赋值数量({len(value)})不匹配"
)
for i, v in zip(indices, value):
self.data[i] = v
else:
# 步进为1时,替换整个切片
self.data[start:stop] = value
elif isinstance(key, int):
self.data[key] = value
else:
raise TypeError(f"不支持的索引类型: {type(key)}")
def __delitem__(self, key):
if isinstance(key, slice):
start, stop, step = key.indices(len(self.data))
if step != 1:
# 步进不为1时,按逆序删除以避免索引错位
indices = sorted(range(start, stop, step), reverse=True)
for i in indices:
del self.data[i]
else:
del self.data[start:stop]
elif isinstance(key, int):
del self.data[key]
else:
raise TypeError(f"不支持的索引类型: {type(key)}")
def __repr__(self):
return repr(self.data)
# 测试切片赋值
ml = MutableList(range(10))
print(ml) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
ml[2:5] = [100, 200, 300]
print(ml) # [0, 1, 100, 200, 300, 5, 6, 7, 8, 9]
# 测试步进切片赋值
ml2 = MutableList(range(10))
ml2[1:8:2] = [10, 20, 30, 40]
print(ml2) # [0, 10, 2, 20, 4, 30, 6, 40, 8, 9]
# 测试删除切片
del ml2[1:8:2]
print(ml2) # [0, 2, 4, 6, 8, 9]
注意: 实现步进删除时,必须逆序删除目标索引,否则前面的删除操作会导致后续索引错位。这是实现可变序列协议时极易忽略的细节。
浅拷贝与深拷贝的切片语义
对于可变序列,切片创建副本(浅拷贝);对于不可变序列,切片返回原对象的引用(优化)。理解这一点对自定义容器类很重要:
# 列表切片是浅拷贝
original = [[1, 2], [3, 4], [5, 6]]
sliced = original[:2]
sliced[0][0] = 999
print(original) # [[999, 2], [3, 4], [5, 6]] (内部元素共享)
# 字符串切片不创建副本(不可变对象的优化)
s = "hello" * 1000
s2 = s[:]
print(s2 is s) # True (Python优化:不可变对象切片返回自身)
五、一维切片到多维切片:元组索引
Python的内置序列类型只支持一维切片。但通过在 __getitem__ 中处理tuple类型的key,我们可以实现多维切片——这正是NumPy数组的核心机制。
多维切片的原理
当使用 obj[1:5, 2:8] 这样的语法时,Python解释器会将逗号分隔的索引项打包成一个元组,等价于 obj.__getitem__((slice(1, 5), slice(2, 8))):
# Python如何解析多维索引
import dis
def test():
x = [1, 2, 3]
y = x[1:3, 2:5] # 语法上可行,但普通列表会报错
# 查看字节码
# dis.dis(test) # 实际上会编译为:y = x.__getitem__((slice(1,3), slice(2,5)))
实现一个支持多维切片的矩阵类
class Matrix:
"""支持多维切片的矩阵类"""
def __init__(self, rows, cols, data=None):
self.rows = rows
self.cols = cols
if data:
self._data = [list(row) for row in data]
else:
self._data = [[0] * cols for _ in range(rows)]
def __getitem__(self, key):
if isinstance(key, tuple):
# 多维索引:key 是包含多个索引项的元组
row_key, col_key = key
# 处理行索引
if isinstance(row_key, slice):
row_indices = range(*row_key.indices(self.rows))
else:
row_indices = [row_key] if isinstance(row_key, int) else list(row_key)
# 处理列索引
if isinstance(col_key, slice):
col_indices = range(*col_key.indices(self.cols))
else:
col_indices = [col_key] if isinstance(col_key, int) else list(col_key)
# 提取子矩阵
result_data = []
for r in row_indices:
row = []
for c in col_indices:
row.append(self._data[r][c])
result_data.append(row)
if isinstance(row_key, int) and isinstance(col_key, int):
return result_data[0][0] # 返回标量
elif isinstance(row_key, int):
return result_data[0] # 返回行向量
else:
return Matrix(len(result_data), len(result_data[0]), result_data)
elif isinstance(key, slice):
# 一维切片:按行切片
row_indices = range(*key.indices(self.rows))
result_data = [self._data[r][:] for r in row_indices]
return Matrix(len(result_data), self.cols, result_data)
elif isinstance(key, int):
return self._data[key][:]
raise TypeError(f"不支持的索引类型: {type(key)}")
def __setitem__(self, key, value):
if isinstance(key, tuple):
row_key, col_key = key
if isinstance(row_key, int) and isinstance(col_key, int):
self._data[row_key][col_key] = value
return
if isinstance(row_key, slice):
row_indices = list(range(*row_key.indices(self.rows)))
else:
row_indices = [row_key] if isinstance(row_key, int) else list(row_key)
if isinstance(col_key, slice):
col_indices = list(range(*col_key.indices(self.cols)))
else:
col_indices = [col_key] if isinstance(col_key, int) else list(col_key)
if isinstance(value, Matrix):
value_data = value._data
elif isinstance(value, (list, tuple)):
value_data = value
else:
value_data = [[value] * len(col_indices)] * len(row_indices)
for i, r in enumerate(row_indices):
for j, c in enumerate(col_indices):
self._data[r][c] = value_data[i][j] if isinstance(value_data[i], (list, tuple)) else value_data[i]
else:
raise TypeError("多维矩阵仅支持元组索引")
def __repr__(self):
rows_str = []
for row in self._data:
rows_str.append(" " + str(row))
return "Matrix([\n" + ",\n".join(rows_str) + "\n])"
# 测试多维切片
m = Matrix(5, 5, [
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]
])
print("原始矩阵:")
print(m)
print()
print("m[1:3, 1:4]:")
print(m[1:3, 1:4])
print()
print("m[2, :]:")
print(m[2, :])
print()
print("m[1:4:2, :3]:")
print(m[1:4:2, :3])
print()
# 多维切片赋值
m[1:3, 1:3] = Matrix(2, 2, [[99, 99], [99, 99]])
print("赋值后的矩阵:")
print(m)
设计要点: 实现多维切片时,关键在于正确处理key为元组的情况,将每个维度的索引项独立解析(支持int和slice),然后从底层数据结构中提取对应的子集。返回值的类型也需要根据索引的维度进行判断:全部为整数索引时返回标量,部分为切片时返回子矩阵。
六、Ellipsis(...)省略号在切片中的应用
Ellipsis是Python的内置常量,写作三个点 ...,其类型为 ellipsis。在多维切片中,Ellipsis(省略号)表示"填充剩余维度",相当于在对应位置插入所需的全部 : 切片。这是NumPy等科学计算库中的核心语法糖。
Ellipsis的基本用法
# Ellipsis 在多维切片中的语义
import numpy as np
arr = np.arange(24).reshape(2, 3, 4)
print("形状:", arr.shape) # (2, 3, 4)
# 以下两种写法等价:
print(arr[0, ..., 0]) # 取第0个批次的第0列全部行
print(arr[0, :, 0]) # 同上
# 以下两种写法等价:
print(arr[..., 0]) # 所有批次所有行的第0列
print(arr[:, :, 0]) # 同上
# Ellipsis 可以出现在任意位置
print(arr[0, ...]) # 等价于 arr[0, :, :]
print(arr[..., 1:3]) # 等价于 arr[:, :, 1:3]
自定义类中处理Ellipsis
class Tensor:
"""自定义张量类,支持Ellipsis切片"""
def __init__(self, shape, data=None):
self.shape = shape
self._dims = len(shape)
if data is not None:
self._data = data
else:
self._data = [0] * self._product(shape)
self._strides = self._compute_strides(shape)
def _product(self, shape):
result = 1
for s in shape:
result *= s
return result
def _compute_strides(self, shape):
strides = [1]
for s in reversed(shape[1:]):
strides.insert(0, strides[0] * s)
return strides
def _normalize_key(self, key):
"""将包含Ellipsis的索引标准化为完整元组"""
if not isinstance(key, tuple):
key = (key,)
# 统计普通维度和Ellipsis
ellipsis_count = sum(1 for k in key if k is Ellipsis)
if ellipsis_count > 1:
raise IndexError("索引中只能有一个省略号(...)")
if ellipsis_count == 1:
# 计算需要补充的 : 数量
explicit = [k for k in key if k is not Ellipsis]
remaining = self._dims - len(explicit)
# 展开省略号
new_key = []
seen_ellipsis = False
for k in key:
if k is Ellipsis and not seen_ellipsis:
new_key.extend([slice(None)] * remaining)
seen_ellipsis = True
elif k is not Ellipsis:
new_key.append(k)
# 如果没有省略号,补充完整的 :
key = tuple(new_key)
elif len(key) < self._dims:
# 没有Ellipsis但维度不足:补充后面的维度
key = tuple(key) + tuple([slice(None)] * (self._dims - len(key)))
return key
def __getitem__(self, key):
key = self._normalize_key(key)
if not isinstance(key, tuple):
key = (key,)
# 计算每个维度的索引范围
ranges = []
for k, dim_size in zip(key, self.shape):
if isinstance(k, slice):
ranges.append(list(range(*k.indices(dim_size))))
elif isinstance(k, int):
ranges.append([k])
else:
raise TypeError(f"不支持的索引类型: {type(k)}")
# 提取数据
result = []
self._extract_recursive(ranges, 0, 0, result)
# 确定返回形状
result_shape = tuple(len(r) for r, k in zip(ranges, key)
if isinstance(k, slice))
if not result_shape: # 全整数索引,返回标量
return result[0] if result else 0
elif len(result_shape) == 1:
return result
else:
return Tensor(result_shape, result)
def _extract_recursive(self, ranges, dim, offset, result):
if dim == len(ranges):
result.append(self._data[offset])
return
for i in ranges[dim]:
self._extract_recursive(ranges, dim + 1,
offset + i * self._strides[dim], result)
def __repr__(self):
return f"Tensor(shape={self.shape}, data={self._data})"
# 测试Ellipsis
t = Tensor((2, 3, 4), list(range(24)))
print("原始形状:", t.shape)
# 使用Ellipsis
result1 = t[0, ...] # 等价于 t[0, :, :]
print("t[0, ...]:", result1)
result2 = t[..., 0] # 等价于 t[:, :, 0]
print("t[..., 0]:", result2)
result3 = t[0, ..., 1:3] # 等价于 t[0, :, 1:3]
print("t[0, ..., 1:3]:", result3)
理解Ellipsis: Ellipsis本质上是一个语法填充器,它在多维切片中自动展开为所需数量的 : 切片。当维度较多时(例如一个4维或5维张量),使用Ellipsis可以大幅简化索引表达式,避免书写大量冒号。
七、自定义切片类实现
除了利用 __getitem__ 处理slice参数外,我们还可以自定义切片类来扩展Python的切片能力。通过继承 slice 或实现 __index__ 协议,可以创建带有额外元数据的切片对象。
带标签的切片类
class LabeledSlice:
"""带有标签和元数据的切片"""
def __init__(self, start=None, stop=None, step=None, label=None):
self._slice = slice(start, stop, step)
self.label = label or ""
@property
def start(self):
return self._slice.start
@property
def stop(self):
return self._slice.stop
@property
def step(self):
return self._slice.step
def indices(self, length):
return self._slice.indices(length)
def __repr__(self):
return (f"LabeledSlice({self.start}, {self.stop}, {self.step}, "
f"label={self.label!r})")
class NamedSlice:
"""支持按名称切片的容器"""
def __init__(self, data, names):
self.data = data
self.names = names # 名称到索引的映射
def __getitem__(self, key):
if isinstance(key, slice):
return NamedSlice(self.data[key], self.names)
elif isinstance(key, str):
# 支持通过名称获取
idx = self.names[key]
if isinstance(idx, slice):
return self.data[idx]
return self.data[idx]
elif isinstance(key, (list, tuple)):
return [self.data[i] for i in key]
return self.data[key]
def __repr__(self):
return f"NamedSlice({self.data})"
# 测试命名切片
ns = NamedSlice(
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
{"前三": slice(0, 3), "后五": slice(5, 10), "中段": slice(2, 8)}
)
print(ns["前三"]) # NamedSlice([10, 20, 30])
print(ns["后五"]) # NamedSlice([50, 60, 70, 80, 90, 100])
print(ns["中段"]) # NamedSlice([30, 40, 50, 60, 70, 80])
切片工厂:基于谓词的智能切片
class PredicateSlice:
"""基于谓词(条件函数)的智能切片"""
def __init__(self, data):
self.data = data
def __getitem__(self, predicate):
"""predicate 是一个函数,返回True的元素被选中"""
return [item for item in self.data if predicate(item)]
class SmartList:
"""支持谓词切片的智能列表"""
def __init__(self, data):
self.data = list(data)
@property
def where(self):
"""返回PredicateSlice,支持 lst.where[lambda x: x > 5] 语法"""
return PredicateSlice(self.data)
def __getitem__(self, key):
if callable(key):
return [x for x in self.data if key(x)]
return self.data[key]
def __repr__(self):
return repr(self.data)
# 测试谓词切片
sl = SmartList(range(20))
print(sl[lambda x: x > 15]) # [16, 17, 18, 19]
print(sl[lambda x: x % 3 == 0]) # [0, 3, 6, 9, 12, 15, 18]
print(sl[lambda x: 5 <= x <= 10]) # [5, 6, 7, 8, 9, 10]
print(sl.where[lambda x: x % 2 == 0]) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
实践模式: 谓词切片虽然不是Python切片协议的正式部分,但它利用 __getitem__ 可接受任意类型参数的特性,实现了类似SQL WHERE子句的筛选语义。这种模式在pandas等数据处理库中有广泛应用。
八、切片在数据框/矩阵/时间序列中的高级应用
切片在数据处理和科学计算领域有着极其广泛的应用。本节以pandas数据框和NumPy矩阵为例,展示切片在实际数据分析中的高级用法。
数据框的多维切片(pandas风格)
pandas的DataFrame同时支持基于标签(loc)和基于位置(iloc)的切片,二者有不同的边界语义:
import pandas as pd
import numpy as np
# 创建示例数据框
df = pd.DataFrame(
np.random.randn(10, 4),
columns=['A', 'B', 'C', 'D'],
index=pd.date_range('2026-01-01', periods=10, freq='D')
)
print("原始数据框:")
print(df.head())
print()
# iloc:基于整数位置的切片
print("iloc切片 (前3行, 第1-2列):")
print(df.iloc[:3, :2])
print()
# loc:基于标签的切片(注意:loc包含终点)
print("loc切片 (从2026-01-01到2026-01-05):")
print(df.loc['2026-01-01':'2026-01-05', ['A', 'C']])
print()
# 布尔索引(谓词切片)
print("A列 > 0 的行:")
print(df.loc[df['A'] > 0, :])
loc和iloc的区别体现了切片的两种语义模型:loc遵循"包含终点"的标签语义(label-based),iloc遵循"不包含终点"的位置语义(position-based)。理解这一区别对于正确使用pandas的切片至关重要。
时间序列的切片行为
时间序列切片是切片在金融数据分析中的重要应用。pandas的Series支持基于时间的部分字符串匹配切片:
# 创建时间序列
ts = pd.Series(
np.random.randn(100),
index=pd.date_range('2026-01-01', periods=100, freq='h')
)
print("前5个时间点:")
print(ts.head())
print()
# 部分字符串切片
print("2026-01-03 全部数据:")
print(ts['2026-01-03'])
print()
# 范围切片(自动填充)
print("2026-01-01 12:00 到 2026-01-02 06:00:")
print(ts['2026-01-01 12:00':'2026-01-02 06:00'].head())
print()
# 高级切片:按月取数据
monthly = ts.resample('D').mean()
print("日均值前10天:")
print(monthly.head(10))
模拟简化版pandas DataFrame切片
class SimpleDataFrame:
"""简化版DataFrame,演示多维切片的实际应用"""
def __init__(self, data, columns, index=None):
self.columns = list(columns)
self.index = list(index) if index else list(range(len(data)))
self._data = [list(row) for row in data]
self._col_index = {col: i for i, col in enumerate(self.columns)}
def iloc(self, row_spec, col_spec=None):
"""基于整数位置的切片"""
# 解析行
if isinstance(row_spec, slice):
rows = list(range(*row_spec.indices(len(self._data))))
elif isinstance(row_spec, int):
rows = [row_spec]
else:
rows = list(row_spec)
# 解析列
if col_spec is None:
cols = list(range(len(self.columns)))
elif isinstance(col_spec, slice):
cols = list(range(*col_spec.indices(len(self.columns))))
elif isinstance(col_spec, int):
cols = [col_spec]
else:
cols = list(col_spec)
result = []
for r in rows:
result.append([self._data[r][c] for c in cols])
result_cols = [self.columns[c] for c in cols]
result_index = [self.index[r] for r in rows]
if len(rows) == 1 and len(cols) == 1:
return result[0][0]
return SimpleDataFrame(result, result_cols, result_index)
def loc(self, row_spec, col_spec=None):
"""基于标签的切片(包含终点)"""
# 解析行标签
if isinstance(row_spec, slice):
start_idx = 0 if row_spec.start is None else self.index.index(row_spec.start)
stop_idx = len(self.index) - 1 if row_spec.stop is None else self.index.index(row_spec.stop)
step = row_spec.step if row_spec.step is not None else 1
if step > 0:
rows = list(range(start_idx, stop_idx + 1, step))
else:
rows = list(range(start_idx, stop_idx - 1, step))
elif isinstance(row_spec, str):
rows = [self.index.index(row_spec)]
else:
rows = [self.index.index(r) for r in row_spec]
# 解析列标签
if col_spec is None:
cols = list(range(len(self.columns)))
elif isinstance(col_spec, slice):
start = 0 if col_spec.start is None else self._col_index[col_spec.start]
stop = len(self.columns) - 1 if col_spec.stop is None else self._col_index[col_spec.stop]
step = col_spec.step if col_spec.step is not None else 1
if step > 0:
cols = list(range(start, stop + 1, step))
else:
cols = list(range(start, stop - 1, step))
elif isinstance(col_spec, str):
cols = [self._col_index[col_spec]]
else:
cols = [self._col_index[c] for c in col_spec]
result = []
for r in rows:
result.append([self._data[r][c] for c in cols])
result_cols = [self.columns[c] for c in cols]
result_index = [self.index[r] for r in rows]
if len(rows) == 1 and len(cols) == 1:
return result[0][0]
return SimpleDataFrame(result, result_cols, result_index)
def __repr__(self):
lines = []
header = "索引\t" + "\t".join(str(c) for c in self.columns)
lines.append(header)
for idx, row in zip(self.index, self._data):
lines.append(f"{idx}\t" + "\t".join(str(v) for v in row))
return "\n".join(lines)
# 测试简化DataFrame
sdf = SimpleDataFrame(
[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]],
columns=['A', 'B', 'C', 'D'],
index=['a', 'b', 'c', 'd']
)
print("原始DataFrame:")
print(sdf)
print()
print("iloc[:2, :2]:")
print(sdf.iloc(slice(None, 2), slice(None, 2)))
print()
print("loc['b':'d', 'B':'D']:")
print(sdf.loc(slice('b', 'd'), slice('B', 'D')))
核心差异: iloc基于整数位置(不含终点),loc基于标签(含终点)。这一差异是pandas等库中切片语义的关键设计决策。实际开发中,实现标签切片时要特别注意"包含终点"的语义要求,这使得实现比位置切片更复杂。
九、切片的边界行为与步进规律总结
切片看似简单,但其边界行为和步进规律存在一些容易混淆的细节。以下是对这些规律的全面总结。
切片边界行为速查表
表达式
结果
说明
nums[:]
[0,1,2,3,4,5,6,7,8,9]
完整副本/引用
nums[2:6]
[2,3,4,5]
包含start,不包含stop
nums[-5:-1]
[5,6,7,8]
负索引映射为正后,仍然不包含stop
nums[:-3]
[0,1,2,3,4,5,6]
省略start默认为0
nums[-3:]
[7,8,9]
省略stop默认为len
nums[::2]
[0,2,4,6,8]
正步进,每隔一个取一个
nums[1::2]
[1,3,5,7,9]
正步进,从索引1开始
nums[::-1]
[9,8,7,6,5,4,3,2,1,0]
负步进,完整反向
nums[8:2:-1]
[8,7,6,5,4,3]
负步进时start>stop
nums[-1:-5:-1]
[9,8,7,6]
负索引+负步进
nums[100:200]
[]
越界不报错,返回空
nums[100:]
[]
start越界返回空
步进规律
# 正步进规律总结
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 步进 = 1:连续子序列
print(nums[2:7:1]) # [2, 3, 4, 5, 6]
# 步进 = 2:每隔一个取一个
print(nums[2:7:2]) # [2, 4, 6]
# 步进 = 3:每隔两个取一个
print(nums[2:7:3]) # [2, 5]
# 负步进:从右向左
print(nums[7:2:-2]) # [7, 5, 3]
# 步进为 -1:反向连续
print(nums[7:2:-1]) # [7, 6, 5, 4, 3]
# 完全反转
print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
切片赋值行为
# 切片赋值的独特行为
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 1. 替换连续子序列
nums[2:5] = [100, 200]
print(nums) # [0, 1, 100, 200, 5, 6, 7, 8, 9]
# 注意:左右两边元素数量可以不匹配
# 2. 扩展列表(左少右多)
nums[2:2] = [10, 20, 30]
print(nums) # [0, 1, 10, 20, 30, 100, 200, 5, 6, 7, 8, 9]
# 3. 删除切片(赋值为空列表)
nums[2:5] = []
print(nums) # [0, 1, 100, 200, 5, 6, 7, 8, 9]
# 4. 步进切片赋值(数量必须匹配)
nums2 = list(range(10))
nums2[1:8:2] = [10, 20, 30, 40]
print(nums2) # [0, 10, 2, 20, 4, 30, 6, 40, 8, 9]
# nums2[1:8:2] = [10, 20] # 报错:ValueError(数量不匹配)
切片赋值风险: 连续切片(step=1)的赋值不要求左右长度匹配,Python会自动调整列表长度。但步进切片(step≠1)的赋值要求替换数量严格匹配,否则抛出ValueError。这是实现 __setitem__ 时必须处理的重要边界情况。
十、核心要点总结
切片本质: obj[start:stop:step] 语法糖会转换为 obj.__getitem__(slice(start, stop, step)) 调用,理解slice对象是掌握切片协议的关键。
indices方法: slice.indices(length) 返回在当前序列长度下实际遍历的 (start, stop, step) 三元组,自动处理负索引和边界截断,是实现自定义切片的核心工具。
__getitem__分发: 在自定义类中实现切片需要根据key的类型(int、slice、tuple)进行分发处理。tuple类型对应多维切片,Ellipsis类型对应省略号展开。
多维切片: 通过处理元组索引实现多维切片,每个维度可以独立使用int或slice。返回值类型由各维度索引方式共同决定(全int→标量,有slice→子容器)。
Ellipsis展开: 省略号(...)在多维切片中自动展开为对应数量的 : 切片,简化高维数据的索引表达式。
标签vs位置: 数据框切片的关键设计决策:loc遵循包含终点的标签语义,iloc遵循不包含终点的位置语义。
边界规则: 切片不会抛出IndexError——超界自动截断。正步进时 start >= stop 返回空,负步进时 start <= stop 返回空。
赋值规则: 连续切片(step=1)赋值不要求长度匹配;步进切片赋值必须长度严格一致。
十一、进一步思考
Python的切片协议是一个优雅的设计范例:既提供了简洁直观的语法糖,又通过底层协议保持了足够的扩展性。通过深入理解这一机制,开发者不仅可以更好地使用Python的内置序列类型,还可以为自己的自定义数据结构赋予与内置类型一致的切片语义。
扩展方向:
惰性切片: 参考NumPy的视图(view)语义,实现不复制数据的惰性切片,父对象数据变化时切片结果也随之变化。
链式切片: 设计支持 obj[1:5][:3][::2] 链式调用的容器,每次返回仍支持切片的新对象。
异步切片: 在异步编程中实现支持await的切片操作,适用于流式数据的分段读取。
表达式切片: 探索使用 >、< 等符号重载实现类似 obj[>5, <10] 的领域特定切片语法。
掌握切片协议的深层原理,意味着我们能够在需要时脱离Python内置序列类型的限制,构建出完全自定义的、具有丰富切片语义的数据结构。这对于科学计算、数据分析、机器学习等领域的开发尤为重要——事实上,NumPy、pandas、xarray等核心科学计算库的底层都离不开对切片协议的深入实现。
"切片的优雅之处在于:它用简单的冒号语法,隐藏了复杂的边界计算,让开发者可以专注于'取什么'而不是'怎么取'。"