为啥切片和范围上限是排他的?

Posted

技术标签:

【中文标题】为啥切片和范围上限是排他的?【英文标题】:Why are slice and range upper-bound exclusive?为什么切片和范围上限是排他的? 【发布时间】:2012-07-07 01:12:06 【问题描述】:

免责声明:我不是在问如果slice()range() 的上限stopargument 是独占的,或者如何 使用这些功能。

rangeslice函数的调用,以及切片符号[start:stop]都指的是整数集。

range([start], stop[, step])
slice([start], stop[, step])

在所有这些中,stop 整数被排除在外。

我想知道为什么语言是这样设计的。

start等于0或被省略时,是使stop等于表示的整数集合中的元素个数吗?

是否有:

for i in range(start, stop):

看起来像下面的 C 代码?

for (i = start ; i < stop; i++) 

【问题讨论】:

密切相关:Numpy Indexing - Questions on Odd Behavior/Inconsistencies 这里讨论一下为什么 Python 使用半开区间:groups.google.com/forum/?fromgroups#!msg/comp.lang.python/… 不管它们为何如此,如果您非常需要该功能,您始终可以编写自己的具有包容性的类似功能。 这里是 Edsger Dijkstra 对为什么半开零基区间约定是计算机编程的最佳选择的可爱手写解释:cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF 我太老了,不能再关心这个行业的为什么了。如果有太多人不得不问为什么,那么您可能正在处理一场宗教战争。我希望得到如何我如何轻松获得替代行为的答案,因为(在宗教战争之外)理性的人可以并且确实会有所不同。 ***.com/questions/29596045/… 【参考方案1】:

documentation 暗示它有一些有用的属性:

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

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

对于非负索引,如果两个索引都在范围内,则切片的长度是索引的差。比如word[1:3]的长度就是2

我认为我们可以假设范围函数的行为相同以保持一致性。

【讨论】:

让我感到困惑的是,对于数组 x,x[-1] 指的是最后一个元素,x[-2:-1] 不是指最后两个元素,而是而只是倒数第二个元素。特别是对于 Ruby 程序员来说,这是一个常见的陷阱,因为您习惯于将 -1 作为最后一个元素并且 .. 表示法包含在内,即 x[-2..-1] 返回最后 2 个元素。 python 冒号 ':' 实际上是 ruby​​ 三点 '...' 你知道什么更好吗?像正常人一样从一个开始计数,并且像正常人一样有闭合的间隔..那么如果你想从 arr = [1,2,3,4,5,6,7,8] 说第二个到第四个......你会切片 arr [2:4]。 .. not arr[1:4] 其中 1 表示第二,4 表示第五但不包括在内.. JEEZUS! @JsonKody 0. 程序员几乎不是“普通人”。 1. 无论如何,“正常”可能取决于你来自哪里。在某些地方,他们将第一层视为离地面 10 到 12 英尺的那一层。【参考方案2】:

这是某个 Google+ 用户的opinion:

[...] 我被半开音程的优雅所左右。特别是 不变的是,当两个切片相邻时,第一个切片的结尾 index 是第二个切片的起始索引太漂亮了 忽视。例如,假设您将一个字符串拆分为三个部分 索引 i 和 j - 部分将是 a[:i]、a[i:j] 和 a[j:]。

Google+ 已关闭,因此链接不再可用。剧透警告:那是 Guido van Rossum。

【讨论】:

web.archive.org/web/20190321101606/https://plus.google.com/… 这是我看到的唯一让我感觉更好的解释。这种优雅是一种非武断的理由,最终给了我一些平静。毕竟,它被称为切片,这清楚地表明其意图就是为了这个,而不仅仅是子集选择。谢谢。 你知道什么更好吗?像正常人一样从一个开始计数,并且像正常人一样有闭合的间隔..那么如果你想从 arr = [1,2,3,4,5,6,7,8] 说第二个到第四个......你会切片 arr [2:4]。 .. 不是 arr[1:4] 其中 1 表示第二个,4 表示第五个但不包括在内.. JEZUS!但实际上,OFFSETS 和 INDEXES 是不同的东西,不应该混淆:((遗憾的是所有主流语言都这样做) 这个解释也让我感觉好一点;然而,对于一种被设计为可读性的语言,它可能仍然是不可原谅的......【参考方案3】:

优雅 VS 明显

说实话,我认为 Python 中的切片方式非常违反直觉,它实际上是在用更多的大脑处理来交换所谓的 优雅,这就是为什么你可以看到this *** article 有超过 2Ks 的点赞,我想是因为很多人一开始并不理解。

举个例子,下面的代码已经让很多 Python 新手头疼了。

x = [1,2,3,4]
print(x[0:1])
# Output is [1]

不仅难以处理,而且难以正确解释,例如上面代码的解释是取第0个元素直到第一个元素之前的元素

现在看看使用上限包容性的 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]

显然,代码不太优雅,但这里没有太多的大脑处理。

结论

归根结底,这真的是优雅 VS 明显的问题,Python 的设计者更喜欢优雅而不是明显。为什么?因为Zen of Python 声明美胜于丑

【讨论】:

我同意从零开始的索引 (ZBI)起初并不明显。我记得几十年前我第一次学习编程时对 ZBI 感到有些困惑。问题不在于唯一的上限概念,而是没有解释该概念的使用这一事实。但是一旦我弄清楚了它的用途就变得很明显了!所以也许“显而易见”在旁观者的眼中,或者说另一种(更优雅的:-)方式:显而易见的是,除非有人简单地表达出来,否则永远不会看到。如果 Python 教科书和教程能简单地表达这一点就好了。 我更喜欢基于 1 的索引和闭区间 .. 这很简单,而且是真正的索引。具有半开间隔的基于零的事物只是偏移量。适用于指针运算等某些情况。不利于正常使用数组(大多数高级公关语言甚至不能进行指针运算,所以这只是不必要的头痛) 如果你想从 arr = [1,2,3,4,5,6,7,8] 说第二个到第四个 .. 你可以切片 arr[2:4] 或者如果你想要第三个元素是 arr[3]。不是 arr[2] 或 arr[1:4] 其中 1 表示第二个,4 表示第五个但不包括在内 -> 那不优雅,那很愚蠢 @JsonKody 在我看来,您实际上是在谈论 counting,而不是索引。但是,如果您“喜欢更多基于 1 的索引和封闭区间”……请使用 BASIC。更好的是,帕斯卡;它会让你分别决定每个数组的起始索引。零?一? -57?所有有效的选择。【参考方案4】:

这个问题有点晚了,不过,这试图回答您问题的为什么部分:

部分原因是因为我们在寻址内存时使用从零开始的索引/偏移量。

最简单的例子是数组。将“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 生成,因此第一项位于 base memory location 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 这样处理指针的语言进行编程时 更直接,或者更直接地组装,这个base+offset方案 变得更加明显。

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

您可能会在 Wikipedia 上的 Zero-based numbering 和 this question from Software Engineering SE 上发现这篇文章很有趣。

示例

例如,在 C 中,如果您有一个数组 ar 并将其下标为 ar[3],这实际上等同于获取数组 ar 的(基)地址并将 3 添加到它 => @987654331 @ 可能会导致这样的代码打印数组的内容,显示简单的 base+offset 方法:

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

真的等价于

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

【讨论】:

这可以解释为什么 range(num) 不包括上限,因为您可以说 num 只是基于 0 的范围数量。它没有解释为什么 range(lower,upper) 不包括它,因为我们特别要求上限 @YonatanNir 这是相同的推理,也是为了一致性。否则,根据是否提供默认值,您将拥有具有相同名称但行为不同的函数。即,对于增加范围,range(num) 实际上与 range(0, num) 和 range(0, 1, num) 相同。 (对于 API 开发人员和使用 API 的人)更容易获得一致的行为。【参考方案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 &lt; 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 视为独占上限。尝试使其使用包容性上限仍然会遇到我前面提到的一些问题。寓意是包容性的上限通常很麻烦。)

(大部分改编自an old post of mine about inclusive upper-bounds in another scripting language。)

【讨论】:

【参考方案6】:

这个上限排除极大地提高了代码理解。我希望它适用于其他语言。

【讨论】:

以上是关于为啥切片和范围上限是排他的?的主要内容,如果未能解决你的问题,请参考以下文章

Go 在数组子切片上的内置范围具有不一致的行为

为啥扩展切片分配不如常规切片分配灵活?

为啥我不能用 `copy()` 复制切片?

为啥 Python 3 中的切片仍然是副本而不是视图?

5g物联卡和切片卡的区别

Python pandas 按多个索引范围切片数据帧