为什么 Python 的切片和范围上限是独有的?
- 2025-01-22 08:45:00
- admin 原创
- 93
问题描述:
我知道当我使用range([start], stop[, step])
或 时slice([start], stop[, step])
,stop
值不包含在范围或切片中。
但为什么会这样呢?
是否例如 arange(0, x)
或range(x)
将包含x
许多元素?
它是否与 C 的 for 循环习语并行,即for i in range(start, stop):
表面上类似for (i = start ; i < stop; i++) {
?
另请参阅使用索引向后循环的案例研究:当尝试按降序获取值时,正确设置stop
和值可能会有点棘手。step
解决方案 1:
文档暗示它具有一些有用的属性:
word[:2] # The first two characters
word[2:] # Everything except the first two characters
这是切片操作的一个有用的不变量:
s[:i] + s[i:]
equalss
。对于非负索引,如果两个索引都在界限内,则切片的长度是索引的差值。例如,的长度
word[1:3]
为2
。
我认为我们可以假设范围函数对于一致性起着相同的作用。
解决方案 2:
以下是Guido van Rossum 的观点:
[...] 我被半开区间的优雅所吸引。尤其是当两个切片相邻时,第一个切片的结束索引是第二个切片的起始索引,这个不变量实在太美了,让人无法忽视。例如,假设您将一个字符串在索引 i 和 j 处拆分为三部分 - 这些部分将是 a[:i]、a[i:j] 和 a[j:]。
[Google+ 已关闭,因此链接不再有效。这是存档链接。]
解决方案 3:
优雅 VS 明显
说实话,我认为 Python 中的切片方式相当违反直觉,它实际上是用更多的脑力处理来换取所谓的优雅,这就是为什么你可以看到这篇 StackOverflow 文章有超过 2K 个赞,我想这是因为很多人最初并不理解它。
就比如下面的代码就已经让很多Python新手头疼不已了。
x = [1,2,3,4]
print(x[0:1])
# Output is [1]
它不仅难以处理,也很难得到适当的解释,例如,上面代码的解释是取第零个元素直到第一个元素之前的元素。
现在看看使用上限包容的 Ruby。
x = [1,2,3,4]
puts x[0..1]
# Output is [1,2]
坦率地说,我确实认为 Ruby 的切片方式对大脑更有益。
当然,当您根据索引将列表分成两部分时,独占上限方法会产生更好看的代码。
# Python
x = [1,2,3,4]
pivot = 2
print(x[:pivot]) # [1,2]
print(x[pivot:]) # [3,4]
现在让我们看看包容性上限方法
# Ruby
x = [1,2,3,4]
pivot = 2
puts x[0..(pivot-1)] # [1,2]
puts x[pivot..-1] # [3,4]
显然,代码不太优雅,但这里不需要进行太多的脑力处理。
结论
归根结底,这其实是一个优雅与显而易见的问题,而 Python 的设计者更喜欢优雅而不是显而易见。为什么?因为Python 之禅说,美丽胜过丑陋。
解决方案 4:
这试图回答你的问题的为什么部分:
部分原因是我们在寻址内存时使用基于零的索引/偏移量。
最简单的例子是数组。将“包含 6 个项目的数组”视为存储 6 个数据项的位置。如果此数组的起始位置位于内存地址 100,则数据(假设为 6 个字符“apple\0”)的存储方式如下:
memory/
array contains
location data
100 -> 'a'
101 -> 'p'
102 -> 'p'
103 -> 'l'
104 -> 'e'
105 -> ' '
因此对于 6 个项目,我们的索引从 100 到 105。地址是使用基数 + 偏移量生成的,因此第一个项目位于基本内存位置100 +偏移量0(即 100 + 0),第二个项目位于 100 + 1,第三个项目位于 100 + 2,...,直到 100
5 是最后一个地点。
这是我们使用从零开始的索引的主要原因,并导致了诸如for
C 中的循环之类的语言构造:
for (int i = 0; i < LIMIT; i++)
或者用 Python 来写:
for i in range(LIMIT):
当您使用 C 语言等更直接地处理指针的语言进行编程时,或者使用汇编语言进行编程时,这种基址+偏移量方案变得更加明显。
由于上述原因,许多语言构造自动使用从开始到长度-1 的范围。
您可能会发现 Wikipedia 上的这篇有关从零开始的编号的文章很有趣,并且还有来自软件工程 SE 的这个问题。
例子:
例如在 C 语言中如果你有一个数组ar
并且你对它进行下标,ar[3]
这实际上相当于获取数组的(基)地址ar
并添加3
到它 =>*(ar+3)
这会导致像这样的代码打印数组的内容,显示简单的基数+偏移量方法:
for(i = 0; i < 5; i++)
printf("%c
", *(ar + i));
确实相当于
for(i = 0; i < 5; i++)
printf("%c
", ar[i]);
解决方案 5:
以下是独占上限是更明智方法的另一个原因:
假设您希望编写一个函数,将某种变换应用于列表中的项子序列。如果间隔按照您的建议使用包含上限,您可能会天真地尝试将其写为:
def apply_range_bad(lst, transform, start, end):
"""Applies a transform on the elements of a list in the range [start, end]"""
left = lst[0 : start-1]
middle = lst[start : end]
right = lst[end+1 :]
return left + [transform(i) for i in middle] + right
乍一看,这似乎是简单而正确的,但不幸的是,它却暗藏错误。
如果发生以下情况会发生什么:
start == 0
end == 0
end < 0
? 一般而言,可能还有更多边界情况需要考虑。谁愿意浪费时间考虑所有这些问题?(这些问题的出现是因为使用包含下限和上限时,没有固有的方式来表达空区间。)
相反,通过使用上限独占的模型,将列表分成单独的片段更简单、更优雅,并且因此更不容易出错:
def apply_range_good(lst, transform, start, end):
"""Applies a transform on the elements of a list in the range [start, end)"""
left = lst[0:start]
middle = lst[start:end]
right = lst[end:]
return left + [transform(i) for i in middle] + right
(请注意,apply_range_good
不会转换lst[end]
;它也将其视为end
独占上限。尝试使其使用包含上限仍然会出现我之前提到的一些问题。寓意是包含上限通常很麻烦。)
(大部分改编自我的一篇关于另一种脚本语言中的包含上限的旧帖子。)
解决方案 6:
埃兹格·迪杰斯特拉 (Edsger Dijkstra) 笔记摘要《为什么编号应该从零开始》。
范围约定
有 4 种惯例来表示自然数 a、a + 1 、 … 、b的范围:
a≤i < b + 1 。
a − 1 < i ≤ b。
a≤i≤b。
a − 1 < i < b + 1。
像范围惯例 2 和 4 中那样排除下限会给出下限 -1(不是自然数),以表示从 0 开始的范围(惯例 2:-1 < i ≤ b;惯例 4:-1 < i < b + 1)。像范围惯例 2 和 3 中那样包括上限会给出负上限(不是自然数),以表示从 0 开始且已缩小到空范围的范围(惯例 2:-1 < i ≤ b,其中b < 0;惯例 3:0 ≤ i ≤ b,其中b < 0)。像范围约定 1 中一样包含下限并排除上限,将给出一个下限 0(自然数),表示从 0 开始的范围(0 ≤ i < b + 1),以及一个上限 0(自然数),表示从 0 开始缩小到空范围(0 ≤ i < 0)。因此,范围约定 1 应为首选。
索引约定
索引n 个元素有两种约定:
从 0 开始。
从 1 开始。
像索引约定 2 中那样从 1 开始,则索引范围为 1 ≤ i < n + 1,遵循首选范围约定。像索引约定 1 中那样从 0 开始,则索引范围为 0 ≤ i < n,遵循首选范围约定,这更好。因此,索引约定 1 应为首选。