当程序需要频繁从序列两端插入或删除元素时,普通 list 往往不是最合适的容器。Python 标准库 collections 提供的 deque,专门支持在头部和尾部高效执行追加与弹出操作,适合实现队列、栈以及有容量上限的历史记录。
为什么两端操作适合使用 deque
list.append() 和 list.pop() 很适合处理列表尾部,但从头部插入或删除元素通常需要移动其余元素。deque 的名称来自 double-ended queue,也就是“双端队列”;它把两端都作为主要操作入口。
常用方法可以分成两组:
| 操作位置 | 添加元素 | 删除并返回元素 |
|---|---|---|
| 右端 | append(value) |
pop() |
| 左端 | appendleft(value) |
popleft() |
下面的程序可以直接运行,观察元素如何从两端进入和离开:
from collections import deque
items = deque(['task-2', 'task-3'])
items.append('task-4')
items.appendleft('task-1')
print('追加后:', list(items))
left_item = items.popleft()
right_item = items.pop()
print('左端取出:', left_item)
print('右端取出:', right_item)
print('剩余元素:', list(items))
运行命令:
python deque_demo.py
预期输出为:
追加后: ['task-1', 'task-2', 'task-3', 'task-4']
左端取出: task-1
右端取出: task-4
剩余元素: ['task-2', 'task-3']
需要注意,空的 deque 调用 pop() 或 popleft() 会抛出 IndexError。消费元素前可以判断容器是否为空,或者在业务层明确处理异常。
用同一个容器实现队列和栈
队列遵循先进先出(FIFO)。生产者从右端加入任务,消费者从左端取出任务即可:
from collections import deque
queue = deque()
for task in ['resize-image', 'send-email', 'write-log']:
queue.append(task)
while queue:
task = queue.popleft()
print(f'执行任务: {task}')
栈遵循后进先出(LIFO)。只操作 deque 的右端,就能得到栈的行为:
from collections import deque
stack = deque()
stack.append('open-project')
stack.append('edit-file')
stack.append('run-tests')
while stack:
action = stack.pop()
print(f'撤销操作: {action}')
两种实现的差别不在容器,而在取出元素的位置。把方向约定写清楚很重要:例如规定生产者只能调用 append(),消费者只能调用 popleft(),可以减少队列代码中左右端混用造成的顺序错误。
用 maxlen 控制历史记录的内存占用
创建 deque 时传入 maxlen,可以得到一个有界双端队列。容器达到上限后,再从一端加入新元素,会自动丢弃另一端最旧的元素。这种行为很适合保存最近的日志、搜索词、指标采样或用户操作。
下面是一个保留最近 3 次请求的完整示例:
from collections import deque
recent_requests = deque(maxlen=3)
for request_id in ['req-101', 'req-102', 'req-103', 'req-104']:
recent_requests.append(request_id)
print(request_id, '->', list(recent_requests))
print('当前历史:', list(recent_requests))
print('容量上限:', recent_requests.maxlen)
最终历史中只会留下 req-102、req-103 和 req-104。调用方不需要在每次写入后手动检查长度并删除旧数据,容量限制直接成为容器的一部分。
不过,自动淘汰也意味着数据会无提示地离开容器。如果历史记录涉及审计、计费或不可丢失的业务事件,就不应只依赖 maxlen;应将持久化存储作为事实来源,把 deque 用作内存中的近期窗口。
采用时检查这几个问题
选择 deque 前,可以根据访问模式做判断:
- 主要在尾部追加和弹出,而且经常按索引读取时,
list通常更自然。 - 需要从头部持续取出元素时,使用
deque.popleft()表达队列语义。 - 需要后进先出的处理顺序时,组合
append()与pop()。 - 只关心最近 N 条数据时,在构造函数中设置
maxlen=N。 - 弹出操作可能遇到空容器时,显式执行空值检查或捕获
IndexError。 - 数据不能丢失时,不要把内存中的
deque当成持久化消息队列。
deque 的价值不在于替代所有列表,而在于让“两端操作”成为清晰、直接的代码。当队列方向、容量限制和数据丢失边界都被明确下来,它能用很少的代码解决一类常见的容器问题。