提问人:wap26 提问时间:7/6/2012 最后编辑:Karl Knechtelwap26 更新时间:2/4/2023 访问量:33915
为什么切片和范围上限是独占的?
Why are slice and range upper-bound exclusive?
问:
我知道当我使用 or 时,该值不包括在范围或切片中。range([start], stop[, step])
slice([start], stop[, step])
stop
但为什么它会以这种方式工作呢?
例如,a 或将包含许多元素吗?range(0, x)
range(x)
x
它是否与 C for 循环成语并行,即表面上相似?for i in range(start, stop):
for (i = start ; i < stop; i++) {
另请参阅使用索引向后循环进行案例研究:在尝试按降序获取值时,正确设置停止
和步长
值可能有点棘手。
答:
文档暗示它具有一些有用的属性:
word[:2] # The first two characters
word[2:] # Everything except the first two characters
下面是一个有用的切片操作不变量:等于 .
s[:i] + s[i:]
s
对于非负指数,如果两个指数都在边界内,则切片的长度是指数的差值。例如,的长度为 。
word[1:3]
2
我认为我们可以假设范围函数对一致性的作用相同。
评论
尽管如此,这个问题有点晚了,这试图回答你问题的原因部分:
部分原因是因为我们在寻址内存时使用从零开始的索引/偏移量。
最简单的例子是数组。将“包含 6 个项目的数组”视为存储 6 个数据项的位置。如果此数组的起始位置位于内存地址 100,则数据(假设 6 个字符“apple\0”)存储如下:
memory/
array contains
location data
100 -> 'a'
101 -> 'p'
102 -> 'p'
103 -> 'l'
104 -> 'e'
105 -> '\0'
因此,对于 6 个项目,我们的索引从 100 变为 105。地址是 使用 base + offset 生成,因此第一项位于基本内存位置 100 + offset 0 (即 100 + 0),第二个在 100 + 1,第三个在 100 + 2,...,直到 100 + 5 是最后一个位置。
这是我们使用从零开始的索引的主要原因,并导致
语言结构,例如 C 语言中的循环:for
for (int i = 0; i < LIMIT; i++)
或在 Python 中:
for i in range(LIMIT):
当您使用像 C 这样的语言进行编程时,您需要处理指针 更直接,或者更直接地组装,这个基数+偏移方案 变得更加明显。
由于上述原因,许多语言结构会自动使用从 start 到 length-1 的此范围。
你可能会发现维基百科上这篇关于零基编号的文章很有趣,还有来自软件工程SE的这个问题。
示例:
例如,在 C 语言中,如果你有一个数组,你给它下标,因为这实际上等同于获取数组的(基)地址并添加 =>,这可能会导致这样的代码打印数组的内容,显示简单的基数+偏移量方法:ar
ar[3]
ar
3
*(ar+3)
for(i = 0; i < 5; i++)
printf("%c\n", *(ar + i));
真的相当于
for(i = 0; i < 5; i++)
printf("%c\n", ar[i]);
评论
以下是 Guido van Rossum 的意见:
[...]我被半开间隔的优雅所动摇。尤其是 不变性,当两个切片相邻时,第一个切片的结束 index 是第二个切片的起始索引太漂亮了,以至于无法 忽视。例如,假设您将一个字符串拆分为三个部分,分别位于 索引 i 和 j -- 部分将是 a[:i]、a[i:j] 和 a[j:]。
[Google+ 已关闭,因此链接不再有效。这里有一个存档链接。
评论
以下是排他性上限是更明智的方法的另一个原因:
假设您希望编写一个函数,该函数将某些转换应用于列表中项的子序列。如果间隔按照您的建议使用包容性上限,您可能会天真地尝试将其写成:
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
(主要改编自我的一篇关于另一种脚本语言中的包容性上限的旧文章。
优雅 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 的切片方式对大脑更好。
当然,当您根据索引将列表拆分为 2 个部分时,独占上限方法将导致代码更好看。
# 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 的设计者更喜欢优雅而不是显而易见。为什么?因为蟒蛇的禅宗说,美丽胜于丑陋。
评论
这种上限排除大大提高了对代码的理解。我希望它能出现在其他语言上。
评论