刚接触 Python 的初学者几乎都会产生同一个困惑:
for i in range(1, 8):
print(i) # 输出的是 1 到 7,居然不包含 8!
每次写循环或切片,都要在心里琢磨到底要不要 +1 或者 -1,让人感觉反直觉又容易出错。
但这绝不是 Python 设计师的随性之举,而是计算机科学界历经数十年演进后沉淀出的最优设计公理。
一、 根源:1982 年 Dijkstra 的经典数学推导
关于区间边界的讨论,最严谨的答案早在 1982 年就被图灵奖得主 艾兹格·迪杰斯特拉(Edsger W. Dijkstra)在一篇著名的手稿《Why numbering should start at zero》(为什么编号应从零开始)中彻底终结了。
如果要表示一个从 到 的自然数连续序列,理论上只有四种区间写法:
| 方案 | 形式 | 示例 |
|---|---|---|
| a(左闭右开) | ||
| b(左开右闭) | ||
| c(双闭区间) | ||
| d(双开区间) |
Dijkstra 用了三轮无可辩驳的淘汰逻辑,一步步排除了其他三种形式:
1. 第一轮筛选:计算区间长度(a、b 胜出)
- 采用 a 和 b 时:区间长度直接等于上界减下界()。例如 个元素,心算与计算极其自然。
- 采用 c 和 d 时:长度变成了 或 。每次求长度都要额外进行加减运算,这是工业界无数 差一错误(Off-by-one Error) 的万恶之源。
2. 第二轮筛选:自然数下界的选取(a、c 胜出)
- 自然数有明确的最小值(),但没有最大值。
- 如果下界使用严格小于(像 b 和 d 那样),如果要表示“从 0 开始的 个自然数序列”,必须写成 。
- 这意味着你被迫引入了一个 不属于自然数集的负数()来描述自然数序列,在数学集合定义上非常不自然。因此下界必须是包含的()。
3. 第三轮筛选:空区间的表示(a 最终胜出)
这是最致命的一点:
- 如果选用双闭区间 c,当一个区间收缩为空序列时,应该怎么写?只能写成 。上界比下界还要小,在逻辑语义上极其怪异与反常。
- 而采用左闭右开 a 时,空序列自然地表示为 。下界等于上界即为空,既没有倒挂,也不需要任何特殊标记。
结论:左闭右开 在数学与计算机科学语义上以压倒性优势胜出。
二、 落地工程:日常编码中的三大巨大优势
当把这个理论投射到日常的 Python 编程中时,左闭右开的优势体现得淋漓尽致:
1. 数组切片无缝拼合(最核心特性)
在列表和字符串切片中,左闭右开实现了前所未有的优雅:
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
split_point = 5
left_part = data[:split_point] # [0, 1, 2, 3, 4]
right_part = data[split_point:] # [5, 6, 7, 8, 9]
# 核心等式永远成立:
assert left_part + right_part == data
前一段的终点直接就是后一段的起点!
- 没有任何元素被重复处理,也没有任何元素被漏掉。
- 在二分查找、归并排序、分治算法、并行计算数据切片、数据库翻页等场景中,程序员无需处理烦人的
+1/-1边界,可靠性提升了一个数量级。
2. 与 0 索引(0-based index)完美契合
现代编程语言普遍采用从 0 开始索引。对于一个长度为 的数组:
- 有效下标是 。
- 在左闭右开规则下,直接写
range(N)或range(0, N),最大取到的正是 。不仅遍历了全部元素,还永远不用担心索引越界(IndexError)。
3. 空区间的自然处理
list(range(5, 5)) # []
data[3:3] # []
当起始位置等于结束位置时,天然获得空列表,无需额外的条件判断防护。
三、 一个有趣的特例:为什么 random.randint(1, 6) 包含 6?
细心的开发者可能会发现一个例外:
import random
random.randint(1, 6) # 这里居然是包含 6 的!
这其实是 Python 早期的一个设计历史遗留:
- 早期设计
randint时,开发者为了迎合普通用户的现实生活直觉(如模拟“掷骰子 1 到 6 点”),妥协做成了双闭区间。 - 但这也带来了 API 不一致的隐患。因此 Python 后来引入了规范的
random.randrange(1, 7),严格回归左闭右开。 randint之所以没有被废弃或修改,纯粹是为了向后兼容(Backward Compatibility),避免破坏已有的存量代码。这一反例反而更加印证了:左闭右开才是整门语言真正坚定推崇的核心规范。
四、 工业界的跨语言共识
左闭右开绝不是 Python 的专属,而是现代主流系统与语言的一致选择:
- C++ STL:迭代器的
container.begin()到container.end(),end()始终指向最后一个元素之后的位置。 - Java:
String.substring(beginIndex, endIndex)。 - Go:切片语法
s[low:high]。 - Rust:半开区间语法
0..n。 - 早在 1980 年代施乐帕克实验室(Xerox PARC)开发的 Mesa 语言,在经过全面评估后就明确规定:仅采用左闭右开,禁止使用其他三种区间表示。
五、 结语
下次当你写下 range(1, 8) 时,不必再觉得反直觉。这行看似简单的代码背后,凝聚了四十多年前计算机大师对逻辑纯粹性的严密推演,以及无数工程师用血泪 Bug 换来的最佳实践。
参考与来源
- 知乎原回答:Python 中的 range 函数为什么要设计成“左闭右开”的样子,这是有啥用意吗? - AI解码师的回答 - 知乎
- 手稿出处:Edsger W. Dijkstra, Why numbering should start at zero (EWD 831, 1982)