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

Posted

技术标签:

【中文标题】为啥 Python 3 中的切片仍然是副本而不是视图?【英文标题】:Why are slices in Python 3 still copies and not views?为什么 Python 3 中的切片仍然是副本而不是视图? 【发布时间】:2011-10-17 15:36:46 【问题描述】:

正如我在评论 this answer 后才注意到的,Python 3 中的切片返回它们正在切片的任何内容的浅表副本,而不是视图。为什么还是这样?甚至撇开 numpy 使用视图而不是副本进行切片,dict.keysdict.valuesdict.items 在 Python 3 中都返回视图这一事实,以及 Python 3 的许多其他方面旨在更多地使用迭代器,使得切片看起来会变得相似。 itertools 确实有一个 islice 函数可以进行迭代切片,但这比普通切片更受限制,并且不提供类似于 dict.keysdict.values 的视图功能。

同样,您可以使用对切片的赋值来修改原始列表,但切片本身是副本而不是视图,这是该语言的一个矛盾方面,并且似乎违反了 @987654322 中说明的几个原则@。

也就是说,你可以做到的事实

>>> a = [1, 2, 3, 4, 5]
>>> a[::2] = [0, 0, 0]
>>> a
[0, 2, 0, 4, 0]

但不是

>>> a = [1, 2, 3, 4, 5]
>>> a[::2][0] = 0
>>> a
[0, 2, 3, 4, 5]

或类似的东西

>>> a = [1, 2, 3, 4, 5]
>>> b = a[::2]
>>> b
view(a[::2] -> [1, 3, 5])   # numpy doesn't explicitly state that its slices are views, but it would probably be a good idea to do it in some way for regular Python
>>> b[0] = 0
>>> b
view(a[::2] -> [0, 3, 5])
>>> a
[0, 2, 3, 4, 5]

似乎有些武断/不受欢迎。

我知道http://www.python.org/dev/peps/pep-3099/ 以及它说“切片和扩展切片不会消失(即使__getslice____setslice__ API 可能被替换)的部分,它们也不会返回视图标准对象类型。”,但链接的讨论没有提及为什么做出关于使用视图进行切片的决定;事实上,在原帖中列出的建议中,大多数关于该具体建议的 cmets 似乎都是积极的。

是什么阻止了这样的事情在 Python 3.0 中实现,Python 3.0 专门设计为不严格向后兼容 Python 2.x,因此是实现这种设计更改的最佳时机,是否存在在未来的 Python 版本中有什么可能阻止它吗?

【问题讨论】:

我怀疑答案可能是由于涉及 Python 列表的低级实现(针对速度进行了优化)的技术原因,视图无法正常工作。我将尝试查看源代码以支持这一点。 但是,我们需要 my_list[:] 的替代方案,执行“from copy import shallowcopy”会很糟糕。 @utdemir 另一种选择是list(my_list)。它没有那么漂亮,但仍然相当简洁。 @Wallacoloo:它还允许混合列表和迭代器/生成器/等。 (另一方面,它将任何元组转换为列表,如果您想保持不变性,这可能是不可取的。) 实际上,我怀疑这也会导致在移植到 Python 3 时出现很多细微的错误,即使 2to3 确实处理了它。 【参考方案1】:

看来我找到了意见决定背后的很多原因,通过以http://mail.python.org/pipermail/python-3000/2006-August/003224.html 开头的线程(主要是关于切片字符串,但线程中至少有一封电子邮件提到了可变对象,如列表) ,还有一些来自:

http://mail.python.org/pipermail/python-3000/2007-February/005739.htmlhttp://mail.python.org/pipermail/python-dev/2008-May/079692.html 并在帖子中关注电子邮件

看起来,为基础 Python 切换到这种风格的优势会被诱导的复杂性和各种不受欢迎的边缘情况所抵消。哦,好吧。

...当我开始想知道是否可以将 slice 对象的当前工作方式替换为 itertools.islice 的可迭代形式,就像 zipmap 等一样。在 Python 3 中所有返回迭代而不是列表,我开始意识到所有意外行为和可能由此产生的问题。现在看来这可能是一个死胡同。

从好的方面来说,numpy 的数组相当灵活,所以在可能需要这种事情的情况下,使用一维 ndarray 代替列表不会太难。但是,ndarrays 似乎不支持使用切片在数组中插入额外的项目,就像 Python 列表一样:

>>> a = [0, 0]
>>> a[:1] = [2, 3]
>>> a
[2, 3, 0]

我认为 numpy 的等价物应该是这样的:

>>> a = np.array([0, 0])  # or a = np.zeros([2]), but that's not important here
>>> a = np.hstack(([2, 3], a[1:]))
>>> a
array([2, 3, 0])

一个稍微复杂一点的案例:

>>> a = [1, 2, 3, 4]
>>> a[1:3] = [0, 0, 0]
>>> a
[1, 0, 0, 0, 4]

>>> a = np.array([1, 2, 3, 4])
>>> a = np.hstack((a[:1], [0, 0, 0], a[3:]))
>>> a
array([1, 0, 0, 0, 4])

当然,上述 numpy 示例不会像常规 Python 列表扩展那样将结果存储在原始数组中。

【讨论】:

您能否举一个例子说明为什么使用内置 Python 切片执行此操作会产生不良的边缘情况?【参考方案2】:

同样,您可以使用对切片的赋值来修改原始列表,但切片本身是副本而不是视图。

嗯.. 这不太对;虽然我能看出你会怎么想。在其他语言中,切片分配,例如:

a[b:c] = d

等价于

tmp = a.operator[](slice(b, c)) # which returns some sort of reference
tmp.operator=(d)        # which has a special meaning for the reference type.

但在python中,第一条语句实际上是这样转换的:

a.__setitem__(slice(b, c), d)

也就是说一个item assignment其实在python中是被特意识别出来的,具有特殊意义,与item lookup和assignment分开;它们可能无关。这与python整体上是一致的,因为python没有像C/C++中的"lvalues"这样的概念;没有办法重载赋值运算符本身;仅当分配的左侧不是普通标识符时的特定情况。

假设列表有视图;而你尝试使用它:

myView = myList[1:10]
yourList = [1, 2, 3, 4]
myView = yourList

在python以外的语言中,可能有一种方法可以将yourList推入myList,但在python中,由于名称myView作为一个裸标识符出现,它只能表示一个变量assignemnt;视图丢失。

【讨论】:

Numpy 至少通过使用 myView[:] = yourList 来解释裸标识符位。同样的事情可以在普通的 Python 中完成。 a[:]a.__getitem__(slice(None)) 的语法糖,将返回 a 中所有项目的视图(或 a 引用的基础对象中的项目,如果 a 本身是一个视图)而不是浅拷贝a 默认情况下。这本身实际上可能有点不符合 Python 标准,但似乎不会比已经完成的更是如此。 如果你这么喜欢 numpy.. 为什么不直接使用 numpy? np.array((), np.object_) 因为最初的意图是为了更好地与 Python 的内置类型相结合。正如我自己的回答所示,虽然在这种情况下使用 numpy 确实似乎是最好的解决方案,但它仍然不是最优的,除非存在相对简单的方法来通过切片扩展 ndarray 以使对象保持不变,我不知道的。

以上是关于为啥 Python 3 中的切片仍然是副本而不是视图?的主要内容,如果未能解决你的问题,请参考以下文章

没有副本的Python切片? [复制]

Python:无法替换列表中的项目,原因是:TypeError:列表索引必须是整数或切片,而不是 str

为啥我的代码没有过滤并且仍然打印列中的所有内容而不是应该过滤的内容?

精细化python-切片

Redshift:如何将连接表的副本复制到集群中的每个切片上?

为啥我们不能检查索引是不是出现在切片对象中?