为啥通过切片分配到列表末尾之后不会引发 IndexError? [复制]

Posted

技术标签:

【中文标题】为啥通过切片分配到列表末尾之后不会引发 IndexError? [复制]【英文标题】:Why does assigning past the end of a list via a slice not raise an IndexError? [duplicate]为什么通过切片分配到列表末尾之后不会引发 IndexError? [复制] 【发布时间】:2017-03-26 07:21:46 【问题描述】:

我正在处理sparse list implementation,最近通过切片实现了分配。这让我在 Python 的内置 list 实现中发现了 I find suprising 的一些行为。

给定一个空的list 和一个通过切片的赋值:

>>> l = []
>>> l[100:] = ['foo']

我本来希望这里有来自listIndexError,因为实现方式意味着无法从指定的索引中检索项目::

>>> l[100]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

'foo' 甚至无法从指定的切片中检索:

>>> l = []
>>> l[100:] = ['foo']
>>> l[100:]
[]

l[100:] = ['foo'] 追加list(即在此分配之后的l == ['foo']),并且自the BDFL's initial version 以来似乎一直以这种方式运行。我在任何地方都找不到此功能的文档 (*),但 CPython 和 PyPy 都以这种方式运行。

按索引赋值会报错:

>>> l[100] = 'bar'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list assignment index out of range

那么为什么通过切片分配 list 的末尾不会引发 IndexError (或其他错误,我猜)?


为了澄清前两个 cmets,这个问题专门关于 assignment,而不是检索cf. Why substring slicing index out of range works in Python?)。

当我明确指定索引 100 时,尝试猜测并将 'foo' 分配给 l 在索引 0 并不遵循通常的禅宗蟒蛇。

考虑赋值发生在远离初始化并且索引是一个变量的情况。调用者无法再从指定位置检索他们的数据。

list 的结尾之前分配给切片的行为与上面的示例有些不同:

>>> l = [None, None, None, None]
>>> l[3:] = ['bar']
>>> l[3:]
['bar']

(*) 这种行为在官方文档中的Note 4 的5.6. Sequence Types 中定义(感谢elethan),但它没有解释为什么会这样在分配时被认为是可取的。


注意:我了解检索是如何工作的,并且可以看到在分配时与 this 保持一致可能是可取的,但我正在寻找一个被引用的原因,说明为什么分配给切片会在这个大大地。 l[100:]l[100:] = ['foo'] 之后立即返回[]l[3:]l[3:] = ['bar'] 之后返回['bar'] 如果你不知道len(l) 是令人惊讶的,特别是如果你正在关注Python 的EAFP idiom。

【问题讨论】:

a = l[100:] 不会导致错误,只是a == [],这是一个合理的解释,因为100 超出了end 它只返回end。事实上,start &gt; stopstartend 返回空列表的所有切片都较少。 @Johnsyweb。索引是指序列的特定元素,而切片是指序列的结构部分。在包含三个元素的列表中,切片 [1:1] 引用 元素之间的空白部分。它不引用任何特定的索引元素 - 但仍然可以分配给它(有效地执行插入操作)。 注意 4 本节可能会有所帮助:docs.python.org/2/library/… 我同意这有点令人惊讶,但它与“如果切片位置超出列表末尾,则扩展列表”是一致的。但是为什么会这样而不是(比如说)引发IndexError,我不知道。 赋值给切片不引发异常的原因与访问切片不赋值的原因完全相同。 【参考方案1】:

让我们看看实际发生了什么:

>>> l = []
>>> l[100:] = ['foo']
>>> l[100:]
[]
>>> l
['foo']

所以分配实际上是成功的,并且项目被放入列表中,作为第一个项目。

为什么会发生这种情况是因为索引位置的100: 被转换为slice 对象:slice(100, None, None)

>>> class Foo:
...     def __getitem__(self, i):
...         return i
... 
>>> Foo()[100:]
slice(100, None, None)

现在,slice 类有一个方法 indices(不过我无法在网上找到它的 Python 文档),当给定一个序列的长度时,它会给出 (start, stop, stride)该序列的长度。

>>> slice(100, None, None).indices(0)
(0, 0, 1)

因此,当将此切片应用于长度为 0 的序列时,它的行为与用于切片 retrievals 的切片 slice(0, 0, 1) 完全相同,例如当foo 是一个空序列时,foo[100:] 不会抛出错误,它的行为就像请求了foo[0:0:1] - 这将导致检索时出现空切片。

现在当 l 是具有100 个以上元素的序列 时,当使用l[100:] 时,setter 代码应该可以正常工作。为了让它在那里工作,最简单的方法是重新发明***,只使用上面的indices 机制。不利的一面是,它现在在边缘情况下看起来有点奇怪,但是对“超出范围”的切片的切片分配将被放置在当前序列的末尾。 (然而,CPython 代码中几乎没有代码重用;list_ass_slice 基本上复制了所有这些索引处理,even though it would also be available via slice object C-API)。

因此:如果切片的起始索引大于或等于序列的长度,则生成的切片的行为就好像它是从序列末尾开始的零宽度切片 >。即:如果a &gt;= len(l)l[a:] 在内置类型上的行为类似于l[len(l):len(l)]。对于每个分配、检索和删除都是如此。

这样做的可取之处在于它不需要任何例外slice.indices 方法不需要处理任何异常 - 对于长度为 l 的序列,slice.indices(l) 将始终导致 (start, end, stride) 可用于任何分配、检索和删除的索引,并且它保证startend 都是0 &lt;= v &lt;= len(l)

【讨论】:

这完美地解释了发生了什么。我不明白的是为什么当指定位置之外的位置时分配到列表末尾是可取的行为。 感谢更新的答案。为什么异常是不可取的?分配、检索和删除之间的一致性当然是可取的。【参考方案2】:

对于索引,如果给定索引超出范围,则必须引发错误,因为没有可以返回的可接受的默认值。 (返回None 是不可接受的,因为None 可能是序列的有效元素。

相比之下,对于切片,如果任何索引超出范围,则不需要引发错误,因为返回空序列作为默认值是可以接受的。这样做也是可取的,因为它提供了一种一致的方式来引用元素之间和序列末端之外的子序列(因此允许插入)。

如Sequence Types Notes 中所述,如果切片的开始或结束值大于len(seq),则使用len(seq)

所以给定a = [4, 5, 6],表达式a[3:]a[100:] 都指向列表中最后一个元素之后的空子序列。但是,在使用这些表达式进行切片分配后,它们可能不再引用同一事物,因为列表的长度可能已更改。

因此,在分配a[3:] = [7] 之后,切片a[3:] 将返回[7]。但是在分配a[100:] = [8] 之后,切片a[100:] 仍然会返回[],因为len(a) 仍然小于100。考虑到上述所有其他内容,如果要保持切片分配和切片检索之间的一致性,这正是人们应该期望的。

【讨论】:

你提出了一些关于默认值和一致性的好观点。

以上是关于为啥通过切片分配到列表末尾之后不会引发 IndexError? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

为啥分配给空列表(例如 [] = "")不会出错?

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

Go 切片内存分配

列表元祖的操作

Python为啥向二维列表中追加元素后所有元素都变一样了

为啥切片 params 散列会对批量分配造成安全问题?