news 2026/5/2 0:57:34

Python(列表进阶)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python(列表进阶)

目录

1.切片---不只是一段子列表

1.切片的内存模型:浅拷贝与共享

2. 切片的实现细节:slice 对象与 __getitem__

3. 步长为负的彻底理解

4. 切片赋值的高级技巧

5. 切片作为视图:memoryview 与 array 模块

为什么切片会创建副本?

如何避免创建副本?

对于不可变序列(如字符串、元组)呢?

NumPy

视图

理解“视图”:数据共享,而非复制

“视图”的核心优势与应用场景

视图的风险:意外修改与内存泄漏

1. 数据被意外修改 (数据泄漏)

2. 可能阻止内存释放 (内存泄露)

核心区别:视图 vs 副本 (View vs Copy)

创建视图的注意事项

视图的实现原理

比较

2.追加 —— 动态扩容的艺术

1.append 的过度扩容(overallocation)机制

2. extend 与 += 的细微区别

3. 预先分配容量以减少扩容

4. append 与列表推导式的取舍

3. 插入 —— 成本高昂的灵活

1. insert 的时间复杂度与内存移动

2. 批量插入:切片赋值 vs 多次 insert

3. 在头部插入的替代方案:collections.deque

4. 插入的实用模式:维持有序列表

4.性能基准与实战建议

1. 简单基准对比

2. 实战建议汇总

5.扩展:自定义序列实现切片逻辑

6.高级陷阱与细节

1. 对同一个列表的多个切片同时赋值

2. 空切片与边界情况

3. list 与 array 的切片复制效率


在入门阶段,我们已经学会了创建列表、按索引访问、简单的增删改查。但要想真正写出高效、优雅的代码,必须深入理解切片追加插入的底层机制、性能特征以及进阶用法。本文假设你已经熟悉列表的基本操作,我们将聚焦于那些容易被忽视的细节、高级技巧和实用模式。

1.切片---不只是一段子列表

1.切片的内存模型:浅拷贝与共享

很多人知道lst[start:stop]返回一个新列表,但新列表中的元素与原列表的关系是什么呢?

  • 不可变对象(int, str, tuple):新列表中的元素是原列表中元素值的副本(实际上是小整数的驻留或字符串的引用,但因为不可变,效果上等价于新值)。

  • 可变对象(list, dict, set 等):新列表中的元素是原列表中对象引用的副本,因此通过新列表修改内部可变对象会影响到原列表。

original = [[1, 2], [3, 4]] sliced = original[:] # 浅拷贝 sliced[0][0] = 99 print(original) # [[99, 2], [3, 4]] —— 原列表也被修改

这就是浅拷贝。要完全独立,需要深拷贝:copy.deepcopy(original)

2. 切片的实现细节:slice对象与__getitem__

当你写lst[1:5:2]时,Python 会创建一个slice(1, 5, 2)对象,然后调用列表的__getitem__方法。这意味着你可以显式创建slice对象,使代码更灵活:

lst = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] # 定义一个列表 indices = slice(1, 5, 2) print(lst[indices]) # 等价于 lst[1:5:2]

3. 步长为负的彻底理解

负步长意味着从右向左提取,此时开始索引必须大于结束索引(否则结果为空)。理解取出的顺序:起始位置包括start,然后每次加上步长(负数),直到越过stop(注意stop仍然是不包含的)。

nums = [0, 1, 2, 3, 4, 5] print(nums[4:1:-1]) # 从索引4开始,步长-1,到索引2为止(不包含索引1)-> [4,3,2]

一个常见用途是反转:nums[::-1]从开头到末尾,步长-1,结果整个反转。

4. 切片赋值的高级技巧

我们提到过lst[start:stop] = iterable,但还有一些微妙之处:

  • 右侧可以是任何可迭代对象,不仅限于列表。

  • 如果step不为 1(即扩展切片),则右侧的元素个数必须严格等于切片长度,否则抛出ValueError

nums = [0, 1, 2, 3, 4, 5] # 步长为2,切片长度为3(索引 0,2,4) nums[::2] = [10, 20, 30] # 成功 # nums[::2] = [10, 20] # ValueError: attempt to assign sequence of size 2 to extended slice of size 3

利用这一特性,可以高效地替换偶数位置的元素。

5. 切片作为视图:memoryviewarray模块

切片操作(如lst[1:5:2])确实会创建一个新的列表(对于列表而言),其中包含原列表部分元素的浅拷贝。这种复制行为会消耗额外的内存和时间,对于大列表来说可能代价较大。

为什么切片会创建副本?

Python 的设计选择是:列表切片返回新列表,以保证不可变性(让原列表不受影响)和简化程序逻辑。这是一种安全且直观的做法,但牺牲了性能。

性能影响

  • 时间复杂度:O(k),k 为切片长度(元素个数)。

  • 空间复杂度:新列表占用额外的内存。

例如,对一个包含 100 万个元素的列表取前 50 万个,就会复制 50 万个引用,耗时几十毫秒到几百毫秒,内存翻倍。

如何避免创建副本?

如果你只需要读取切片中的元素而不修改它们,可以使用以下方法避免复制:

方法说明
使用索引访问例如用for i in range(start, end, step):逐个访问。
使用itertools.islice返回一个迭代器,不创建新列表。例如import itertools; s = itertools.islice(lst, 1, 5, 2); for val in s: pass
使用arraynumpy对于数值数组,numpy的切片返回视图(不复制数据)。
只取单元素或少数元素直接用索引,无需切片。

对于不可变序列(如字符串、元组)呢?

  • 字符串切片也会创建新字符串(因为字符串是不可变的,但 CPython 有时会优化,但通常视为 O(k))。

  • 元组切片也会创建新元组。

对于大数组,切片复制代价高。如果操作的是同类型数值,可以使用array模块或numpy的视图(view)概念。Python 内置的memoryview也可以对字节数据进行切片而不复制。

import array a = array.array('i', range(1000000)) # a[0:1000] 会复制1000个整数,消耗内存 # memoryview 可以避免复制,但要求对象支持缓冲区协议

在普通列表上无法实现真正的“视图”,但可以通过itertools.islice获得迭代器,避免内存复制。

NumPy

NumPy 不是 Python 的标准库,需要单独安装。

视图

理解“视图”:数据共享,而非复制

一个“视图”数组和它原始的“基础”数组,在底层共用同一块数据内存。你可以通过一个数组的base属性来快速判断它是不是视图。

  • 如果arr.baseNone:说明这个数组自己拥有数据,是原始数组。

  • 如果arr.base是其他数组对象:说明这个数组只是一个视图,数据来自那个被引用的原始数组。

import numpy as np # 创建一个基础数据 arr = np.array([1, 2, 3, 4, 5]) # 视图:通过切片创建视图 view = arr[1:4] # 副本:显式创建副本 copy = arr.copy() # 检查 .base 属性 print(view.base is arr) # True,验证了它是视图 print(copy.base is None) # True,验证了它是数据拥有者

“视图”的核心优势与应用场景

这种设计的主要优势在于:

  • 性能:创建视图不需要复制数据,因此几乎是即时完成的,这对于处理大型多维数组非常关键。

  • 内存效率:视图与原始数据共享内存,不额外占用空间。这意味着即使创建百万数据量的切片,内存开销也几乎为0

  • 原地修改:通过视图修改数据,会直接反映到原始数组中。

  • 连续内存:NumPy 数组存储在连续内存中,使得切片操作非常高效。

正是为了满足科学计算中对性能内存效率的极致追求,NumPy才选择了与Python列表完全不同的“视图机制”。

视图的风险:意外修改与内存泄漏

使用“视图”机制有两个主要的潜在风险需要留意。

1. 数据被意外修改 (数据泄漏)

因为视图与原始数组共享数据,所以你可能在无意间修改了原始数据。

import numpy as np a2 = np.array([[12, 5, 2, 4], [7, 6, 8, 8], [1, 6, 7, 7]]) # 通过切片创建子数组视图 a2_sub = a2[:2, :2] a2_sub[0, 0] = 99 # 看似只是修改子数组 # 结果:原始数组也被改变了! print(a2) # 输出: # [[99 5 2 4] # [ 7 6 8 8] # [ 1 6 7 7]]

这个例子清楚地展示了通过视图修改数据如何直接影响了原始数组。

2. 可能阻止内存释放 (内存泄露)

视图会“引用”原始数据。如果原始数组很大,而你创建了它的一个小切片视图后,又删除了原始数组引用,只要这个视图还存在,它所引用的整个原始数组内存都无法被释放。这可能导致程序意外地占用大量内存。

核心区别:视图 vs 副本 (View vs Copy)

为了帮助你更清晰地对比,我制作了下面的表格:

特性视图 (View)副本 (Copy)
内存共享原始数组内存,不额外占用分配独立内存,占用空间
性能创建速度极快(O(1)时间)创建速度慢(O(n)时间,n为数据量)
修改影响修改视图会改变原始数组修改副本不影响原始数组
检查arr.base指向原始数组arr.baseNone

创建视图的注意事项

  • 当切片是不连续时:使用“高级索引”(如整数数组索引)时,NumPy很可能会生成一个副本,而不是视图。

  • 当切片导致形状改变时:某些形状的改变也可能导致生成副本。

视图的实现原理

NumPy实现视图的核心机制包括datashapestrides

首先看一下原始数组的信息:

python

import numpy as np a = np.arange(1, 7).reshape(2, 3) print(a.shape) # (2, 3) print(a.strides) # (24, 8) # 从一行开头到下一行开头需跳转24字节,列方向相邻元素间隔8字节

现在,创建视图b = a[1]。NumPy在幕后执行了以下操作:

  1. 计算偏移量offset = 1 * a.strides[0] = 24字节。

  2. 设置数据指针:让视图b.data指向a.data起始地址向后偏移24字节的位置。

  3. 设置新元数据b.shape = (3,)b.strides = (8,)

这样,b就完全没有复制数据,而是通过自己独立的shapestrides,配合指向原始内存区的指针,正确地“解码”出了我们需要的那一维数据。

比较

数据类型/方法切片行为是否复制备注
普通列表listlst[a:b],创建新列表(浅拷贝)通用但消耗内存
array.arrayarr[a:b],也创建新数组(复制元素)比列表紧凑,但仍复制
memoryviewmv[a:b],返回新memoryview视图需要对象支持缓冲区协议(如bytes,bytearray,array.array
numpy.ndarraynarr[a:b](默认返回视图)科学计算常用,支持多维
itertools.isliceislice(seq, a, b),返回迭代器不复制,但只能顺序访问一次,不支持索引

在这些方法里,需要额外安装的只有numpy。其他提到的模块和方法都是 Python 自带的,无须额外安装。

  • NumPy (numpy):需要额外安装。它是 Python 的科学计算第三方库,通常在命令行中使用pip install numpy来安装。

  • array模块:是 Python 的标准库模块,直接import array即可使用。

  • memoryview:是 Python 的一个内置类型,直接使用。

  • itertools模块:是 Python 的标准库模块,直接import itertools即可使用。

  • copy模块:是 Python 的标准库模块,直接import copy即可使用。

  • slice对象:是 Python 的一个内置类型,直接使用。

2.追加 —— 动态扩容的艺术

1.append的过度扩容(overallocation)机制

列表在 CPython 中是一个长度可变的数组。当append触发扩容时,并不是只增加一个元素的空间,而是多分配一些备用容量,以减少未来扩容的次数。策略大约是:new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6),即每次扩容约 12.5% 的额外空间。(跟版本也有关系)

import sys lst = [] print(sys.getsizeof(lst)) # 56 字节(空列表开销) for i in range(10): lst.append(i) print(f"len={len(lst)}, size={sys.getsizeof(lst)}")
  • ys.getsizeof返回对象占用内存字节数(仅容器本身,不包括内部元素对象)。

  • 空列表初始大小(例如56字节)取决于 Python 版本/实现。

  • 列表动态扩容:当向列表追加元素时,如果当前容量不足,Python 会重新分配更大的内存,导致getsizeof返回值增加。

  • 扩容策略(如过量分配)以避免每次追加都重新分配。

  • 注意getsizeof不递归计算元素大小,只是列表对象的内存(包括底层数组的指针数组)。

还需要指出输出模式:开始56,然后随着 len 增加,size 会阶梯式增长。

会发现列表的__sizeof__有时会跳跃式增长。理解这一点有助于评估内存占用,尤其在大量数据时。

2.extend+=的细微区别

lst.extend(iterable)lst += iterable效果相同,都是原地修改。但+运算符(如lst = lst + iterable)会创建一个新列表,效率较低且改变了引用。所以在大列表上使用+=而不是+是一个重要优化。

另外,extend可以接受任何可迭代对象,而+强制要求左右都是列表(或者左列表,右可迭代?实际上lst + [1,2]是合法的,但lst + (1,2)会报错)。extend更通用。

iterable是 Python 中的一个概念,指的是任何可以返回其元素(一次一个)的对象,例如列表、元组、字符串、字典(迭代键)、range 对象、生成器等。它要求对象实现了__iter__()方法或__getitem__()方法,从而可以通过for...in循环遍历。

3. 预先分配容量以减少扩容

如果你要添加大量元素并且知道大致数量,可以预先创建列表并赋值,而不是反复append

# 不推荐 lst = [] for i in range(N): lst.append(i) # 推荐 lst = [0] * N for i in range(N): lst[i] = i

第二种方式避免了多次扩容,通常更快。但需要注意,如果元素是可变对象,[[]]*N会产生共享引用的陷阱。

4.append与列表推导式的取舍

列表推导式不仅更简洁,而且内部实现使用专门的字节码,比显式循环append快:

# 慢 squares = [] for x in range(1000): squares.append(x**2) # 快 squares = [x**2 for x in range(1000)]

这是因为列表推导式避免了每次循环中对append方法的查找和调用开销,并且底层直接构建数组。

3. 插入 —— 成本高昂的灵活

1.insert的时间复杂度与内存移动

insert(i, v)需要将i之后的元素全部向后移动一位,平均时间复杂度 O(n)。对于大型列表,在开头或中间频繁插入会非常慢。在 CPython 中,移动元素是通过memmove完成的,虽然 C 级别很快,但数据量大时仍然有显著开销。

2. 批量插入:切片赋值 vs 多次insert

假设要在索引p处插入k个元素,使用lst[p:p] = [x1, x2, ..., xk]只需一次内存移动(移动len(lst)-p个元素),而每次insert都会移动一次,总复杂度 O(k * n)。因此,切片赋值是批量插入的首选。

3. 在头部插入的替代方案:collections.deque

如果你需要频繁在列表两端添加或删除,应该使用deque(双端队列),它在两端操作都是 O(1)。

appendextendlist类型的方法,也是deque类型的方法(但实现不同)。

appendleftextendleftdeque类型特有的方法,因为只有双端队列才高效支持左侧操作。

这些方法都内建在相应的类中(即它们是内建类型的实例方法),但不是像print那样的内置函数。可以说它们是“内置类型的方法”。

from collections import deque dq = deque([2, 3, 4]) dq.appendleft(1) # O(1) dq.extendleft([0, -1]) # 注意:extendleft 会将参数逆序插入到左侧 print(dq) # deque([-1, 0, 1, 2, 3, 4])

deque也支持insert,但它的insert仍然是 O(n)(因为需要内部移动)。所以大量任意位置插入仍不适合。

4. 插入的实用模式:维持有序列表

当你需要维护一个升序列表并且不断插入新元素时,直接使用list.insert虽然可以,但每次 O(n)。对于大量插入,更好的数据结构是bisect模块 + 列表(查找 O(log n),插入 O(n))或使用heapq(堆)或者sortedcontainers(第三方库)。

import bisect lst = [10, 20, 30] bisect.insort(lst, 25) # 插入后保持有序

bisect.insort内部也是用insert实现的,但至少帮你找到正确位置。

4.性能基准与实战建议

1. 简单基准对比

使用timeit模块可以直观感受不同操作的速度差异

import timeit from collections import deque N = 100000 # 测试 append(每次动态扩容) print(timeit.timeit('for i in range(N): lst.append(i)', setup='lst = []', globals=globals(), number=10)) # 测试预分配后赋值(修正) print(timeit.timeit('for i in range(N): lst[i] = i', setup='lst = [0]*N', globals=globals(), number=10)) # 测试列表头部插入 O(n) print(timeit.timeit('lst = list(range(1000)); lst.insert(0, -1)', number=1000)) # 测试 deque 头部追加 O(1) print(timeit.timeit('dq = deque(range(1000)); dq.appendleft(-1)', setup='from collections import deque', number=1000))

通常预分配比动态append快 10-20%;头部插入列表比 deque 慢几个数量级。

2. 实战建议汇总

场景推荐做法
尾部添加单个元素append
尾部添加多个元素(已知)extend+=
尾部添加多个元素(通过生成器迭代)extend或循环append(后者稍慢但可接受)
头部或中间添加单个元素insert,但注意性能;如果频繁操作,考虑deque
头部或中间添加多个元素切片赋值lst[pos:pos] = items
需要快速查找并插入有序列表bisect.insort
频繁从两端操作deque
需要大量数值运算且关注内存arraynumpy数组(支持切片视图)
需要惰性切片避免复制itertools.islice

5.扩展:自定义序列实现切片逻辑

如果你定义自己的类并希望支持切片,可以实现__getitem____setitem__方法,并处理slice对象。

class MyList: def __init__(self, data): self.data = data[:] def __getitem__(self, key): if isinstance(key, slice): return MyList(self.data[key]) else: return self.data[key] def __setitem__(self, key, value): if isinstance(key, slice): self.data[key] = value else: self.data[key] = value

这样就可以使用obj[1:3]等操作。

6.高级陷阱与细节

1. 对同一个列表的多个切片同时赋值

lst = [0, 0, 0, 0] lst[::2] = [1, 2] # 索引 0,2 变成 1,2 lst[1:3] = [3] # 注意此时列表长度变化

顺序很重要,因为切片赋值会改变列表长度,影响后续切片的索引。尽量避免相互依赖的切片操作同时进行,除非你明确知道顺序后果。

2. 空切片与边界情况

  • lst[5:5] = some在索引 5 处插入(不会删除)。

  • lst[5:4] = ...呢?如果start > stop且步长为正,切片为空,赋值会从左端点开始?实际上,当步长为正时,start必须小于等于stop,否则切片为空且插入位置由start决定(即start处)。这有点反直觉,建议避免使用。

lst = [1,2,3] lst[2:1] = [4] # 在索引 2 处插入,因为空切片位置是 start=2 print(lst) # [1,2,4,3]

这种行为有一定规律,但最好显式使用lst[start:start]。

3.listarray的切片复制效率

array模块的切片返回的新 array 也是复制内存,但如果你使用memoryview,可以做到零复制视图(只对支持缓冲协议的对象,如bytesbytearrayarray)。对于普通列表没有直接视图支持。

import array a = array.array('i', range(1000)) mv = memoryview(a) slice_view = mv[100:200] # 视图,不复制数据 print(slice_view[0]) # 访问第一个元素

感谢你的观看,期待我们下次再见!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 0:55:33

线性自注意力在时间序列预测中的理论与应用

1. 线性自注意力在时间序列预测中的理论基础1.1 自注意力机制的核心思想自注意力机制&#xff08;Self-Attention&#xff09;是Transformer架构的核心组件&#xff0c;其本质是通过计算序列元素间的相关性权重&#xff0c;实现对不同位置信息的动态聚合。在时间序列预测场景下…

作者头像 李华
网站建设 2026/5/2 0:49:31

音乐格式自由转换:浏览器内一键解锁加密音频

音乐格式自由转换&#xff1a;浏览器内一键解锁加密音频 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库&#xff1a; 1. https://github.com/unlock-music/unlock-music &#xff1b;2. https://git.unlock-music.dev/um/web 项目地址: https://gitcod…

作者头像 李华