为什么切片和范围上限是独占的?

Why are slice and range upper-bound exclusive?

提问人:wap26 提问时间:7/6/2012 最后编辑:Karl Knechtelwap26 更新时间:2/4/2023 访问量:33915

问:

我知道当我使用 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++) {


另请参阅使用索引向后循环进行案例研究:在尝试按降序获取值时,正确设置停止步长值可能有点棘手。

Python 语言设计 切片

评论

0赞 Sven Marnach 7/6/2012
密切相关: Numpy 索引 - 关于奇怪行为/不一致的问题
1赞 ecatmur 7/6/2012
以下是关于为什么 Python 使用半开间隔的讨论: groups.google.com/forum/?fromgroups#!msg/comp.lang.python/...
0赞 martineau 7/7/2012
不管它们为什么这样,如果你非常需要该功能,你总是可以编写自己的类似的,这些内容是包容性的。
34赞 Russell Borogove 7/7/2012
以下是 Edsger Dijkstra 可爱的手写解释,解释了为什么半开零基区间约定是计算机编程的最佳选择: cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
2赞 dreftymac 12/5/2019
我太老了,不再关心这个行业的原因了。如果太多人不得不问为什么,那么你可能正在处理一场宗教战争。我所希望的是我如何轻松获得替代行为的答案,因为(在宗教战争之外)理性的人可以而且确实不同。stackoverflow.com/questions/29596045/......

答:

54赞 Toomai 7/6/2012 #1

文档暗示它具有一些有用的属性:

word[:2]    # The first two characters
word[2:]    # Everything except the first two characters

下面是一个有用的切片操作不变量:等于 .s[:i] + s[i:]s

对于非负指数,如果两个指数都在边界内,则切片的长度是指数的差值。例如,的长度为 。word[1:3]2

我认为我们可以假设范围函数对一致性的作用相同。

评论

4赞 farhadf 6/8/2016
让我绊倒的一件事是,对于数组 x,x[-1] 指的是最后一个元素,而 x[-2:-1] 不是指最后 2 个元素,而只是倒数第二个元素。特别是对于 Ruby 程序员来说,这是一个常见的陷阱,因为你习惯于将 -1 作为最后一个元素,而 ..表示法是包容性的,即 x[-2..-1] 返回最后 2 个元素。python 冒号 ':' 实际上是红宝石三点“...”
13赞 Levon 7/7/2012 #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 这样的语言进行编程时,您需要处理指针 更直接,或者更直接地组装,这个基数+偏移方案 变得更加明显。

由于上述原因,许多语言结构会自动使用从 startlength-1 的此范围。

你可能会发现维基百科上这篇关于零基编号的文章很有趣,还有来自软件工程SE的这个问题

示例

例如,在 C 语言中,如果你有一个数组,你给它下标,因为这实际上等同于获取数组的(基)地址并添加 =>,这可能会导致这样的代码打印数组的内容,显示简单的基数+偏移量方法:arar[3]ar3*(ar+3)

for(i = 0; i < 5; i++)
   printf("%c\n", *(ar + i));

真的相当于

for(i = 0; i < 5; i++)
   printf("%c\n", ar[i]);

评论

0赞 CodeMonkey 8/6/2018
这也许可以解释为什么 range(num) 不包括上限,因为您可以说 num 只是基于 0 的范围量。它没有解释为什么范围(下限,上限)不包括它,因为我们特别要求上限
0赞 Levon 8/6/2018
@YonatanNir 这是相同的推理,并且保持一致性。否则,您将拥有同名的函数,这些函数的行为会根据是否提供了默认值而有所不同。也就是说,对于增加范围,range(num) 实际上与 range(0, num) 和 range(0, 1, num) 相同。对于 API 开发人员和使用 API 的人来说,保持一致的行为更容易。
35赞 Nigel Tufnel 1/31/2014 #3

以下是 Guido van Rossum 的意见

[...]我被半开间隔的优雅所动摇。尤其是 不变性,当两个切片相邻时,第一个切片的结束 index 是第二个切片的起始索引太漂亮了,以至于无法 忽视。例如,假设您将一个字符串拆分为三个部分,分别位于 索引 i 和 j -- 部分将是 a[:i]、a[i:j] 和 a[j:]。

[Google+ 已关闭,因此链接不再有效。这里有一个存档链接

评论

1赞 Jason Kelley 4/2/2021
这是我见过的唯一让我感觉更好的解释。这种优雅是一个非武断的理由,它最终给了我一些平静。毕竟,它被称为切片,这清楚地表明其意图只是为了这个,而不仅仅是子集选择。谢谢。
0赞 user3761340 10/5/2021
这个解释也让我感觉好多了;然而,对于一种被设计为可读的语言来说,它可能仍然是不可原谅的......
10赞 jamesdlin 1/25/2015 #4

以下是排他性上限是更明智的方法的另一个原因:

假设您希望编写一个函数,该函数将某些转换应用于列表中项的子序列。如果间隔按照您的建议使用包容性上限,您可能会天真地尝试将其写成:

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_goodlst[end]end

(主要改编自我的一篇关于另一种脚本语言中的包容性上限的旧文章

24赞 Wong Jia Hau 5/7/2018 #5

优雅 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 的设计者更喜欢优雅而不是显而易见。为什么?因为蟒蛇的禅宗,美丽胜于丑陋

评论

4赞 Jon Spencer 8/6/2019
我同意零基指数 (ZBI) 起初并不明显。我记得几十年前,当我第一次学习编程时,我对ZBI有点困惑。问题不在于排他性的上限概念,而在于没有解释该概念的使用。但是一旦我想通了这一点,它的用处就变得很明显了!所以也许“显而易见”在旁观者的眼中,或者说另一个(更优雅:-)方式:显而易见的是,在有人简单地表达它之前,它永远不会被看到。如果 Python 教科书和教程简单地表达这一点,那就太好了。
0赞 JsonKody 8/24/2021
我喜欢更多的基于单的索引和闭合区间..这很愚蠢,很简单,而且是真正的索引。具有半开间隔的从零开始的 THING 只是偏移量。适用于某些情况,如指针、算术等。不利于数组的正常使用(大多数高级公关甚至不能做指针算术,所以这只是不必要的头痛)
0赞 JsonKody 8/24/2021
如果你愿意,让我们说从arr = [1,2,3,4,5,6,7,8] ..你可以切片 arr[2:4],或者如果你想要第三个元素,它将是 arr[3]。不是 arr[2] 或 arr[1:4],其中 1 表示第二,4 表示第五,但不包括在内 -> 这不优雅,那是愚蠢的
0赞 JuSTMOnIcAjUSTmONiCAJusTMoNICa 11/17/2021
@JsonKody 在我看来,你实际上是在谈论计数,而不是索引。但是,如果您“喜欢更多基于单的索引和闭合间隔”......使用 BASIC。更好的是,帕斯卡;它将允许您分别决定每个数组的起始索引。零?一?-57?所有有效选项。
0赞 Rafs 6/16/2022
这难道不与显性比隐性禅宗规则更好相矛盾吗?
-2赞 telepinu 2/25/2021 #6

这种上限排除大大提高了对代码的理解。我希望它能出现在其他语言上。