1. 为什么我会用Class来实现一个栈
先交代一下背景。前段时间我在帮团队做代码审查,发现不少同事在需要"后进先出"语义的业务场景里,直接用Python的list来模拟。比如处理嵌套括号、实现撤销操作、解析表达式时,到处都是 stack = [],然后 stack.append(x)、x = stack.pop()。代码能跑,没有任何问题,但读起来总觉得少了点什么。
真正让我下决心写这篇内容的契机,是看到有人在一个需要维护多个栈的模块里,复制粘贴了三遍同样的栈逻辑——分别处理整数栈、字符串栈和自定义对象栈。每份代码都是二三十行手写的append/pop检查,改一个逻辑要同步改三个地方。这个场景太典型了:当栈的语义从"临时用一下"变成"项目里反复出现的核心结构"时,list裸用就不再是最优解了。
用Class来实现一个栈,核心价值不是"把代码包起来好看",而是三件事:
-
把状态和方法绑在一起。栈的当前容量、栈顶位置、元素列表这些状态,和push/pop/peek这些操作,天然属于同一个概念实体。Class能把它们封装成一个整体,调用方不需要关心内部怎么存,只需要面向接口操作。
-
把重复逻辑收拢到一处。栈的各种边界检查(空栈能不能pop、满栈能不能push、size怎么统计)只需要写一次,所有使用方共享同一份实现。
-
为将来扩展留出口子。哪天你想给栈加一个"返回最小值"的接口,或者把数组存储换成链表存储,只需要改类的内部实现,外部调用代码一行都不用动。
这也是面向对象设计里最基本的一条原则:找到变化的点,封装它。栈的实现方式(数组还是链表)、边界策略(抛异常还是返回None)、容量限制(固定还是动态),这些都是可能变化的点,用Class恰好能把它们隔离在实现细节里。
当然,如果说得直白点——如果你只是写一道算法题,临时用一个list顶一下完全没问题,没必要上纲上线。但当你发现自己在一个真实项目里第三次复制同样的栈逻辑时,就应该停下来,抽象出一个Class了。这也是我这篇文章想帮大家建立的判断力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先定标准:一个合格栈类的最小接口长什么样
在动手写代码之前,我觉得有必要先聊清楚一个问题:什么样的接口才算"够用"?这决定了我们的Class长什么样,也决定了它好不好用。
栈这个数据结构,语义上就两件事:先进后出。它对外暴露的操作,业界基本已经形成了约定俗成的标准。我从使用者的角度倒推一下,一个栈类至少需要这几个方法:
| 方法名 | 功能 | 时间/空间复杂度 | 典型使用场景 |
|---|---|---|---|
push(item) |
将元素压入栈顶 | O(1) | 入栈、记录现场 |
pop() |
弹出并返回栈顶元素 | O(1) | 出栈、取出最近记录 |
peek()(或top()) |
返回栈顶元素但不弹出 | O(1) | 预览、比较栈顶值 |
is_empty() |
判断栈是否为空 | O(1) | 循环条件检查 |
__len__ |
返回栈内元素个数 | O(1) | 统计、遍历控制 |
这五个操作是底线。往下推,还有一些属于"有了更好"的补充:
__repr__/__str__:打印栈的内容,方便调试。__iter__:让栈可以for循环遍历(注意遍历顺序要符合直觉,通常从栈顶开始)。clear():一次性清空栈。extend(iterable):批量压入多个元素。
我说一下为什么是这五个底线操作而不是别的。push和pop是栈的灵魂,没有它们就不叫栈。peek看起来可以不用,但实际上写表达式求值、括号匹配这类算法时,你经常需要"看一眼栈顶是什么但又不能拿掉它",比如匹配到右括号时,需要看栈顶是不是对应的左括号但不急于弹出。is_empty是循环遍历栈时的天然守卫条件,避免你手动维护一个计数器。__len__则让使用者可以用len(stack)这种直觉方式获取大小,而不是调用一个别扭的size()。
还有一个细节值得注意:Python内置的list其实已经把push和pop的O(1)实现得很好了,所以我们在Class内部存储元素时直接用list是明智的——Append和pop在CPython里都是摊销O(1)的操作,我们不需要重复造轮子去实现一个"高性能数组"。
3. 第一版实现:用list做存储的数组栈
确定了接口,代码就水到渠成了。我直接给出一个完整可用的版本,这个版本我已经在很多项目里实测过,稳定可靠。
python复制class Stack:
"""基于Python列表实现的栈结构,支持泛型元素。"""
def __init__(self, initial_items=None):
self._items = list(initial_items) if initial_items else []
def push(self, item):
"""将元素压入栈顶。"""
self._items.append(item)
def pop(self):
"""弹出并返回栈顶元素;若栈为空,抛出IndexError。"""
if self.is_empty():
raise IndexError("pop from an empty stack")
return self._items.pop()
def peek(self):
"""返回栈顶元素,但不弹出。"""
if self.is_empty():
raise IndexError("peek from an empty stack")
return self._items[-1]
def is_empty(self):
"""判断栈是否为空。"""
return len(self._items) == 0
def __len__(self):
"""返回栈内元素个数。"""
