为啥 p[:] 在这两种情况下的工作方式不同?
Posted
技术标签:
【中文标题】为啥 p[:] 在这两种情况下的工作方式不同?【英文标题】:Why was p[:] designed to work differently in these two situations?为什么 p[:] 在这两种情况下的工作方式不同? 【发布时间】:2019-11-08 22:49:23 【问题描述】:p = [1,2,3]
print(p) # [1, 2, 3]
q=p[:] # supposed to do a shallow copy
q[0]=11
print(q) #[11, 2, 3]
print(p) #[1, 2, 3]
# above confirms that q is not p, and is a distinct copy
del p[:] # why is this not creating a copy and deleting that copy ?
print(p) # []
以上确认p[:]
在这两种情况下的工作方式不同。不是吗?
考虑到在以下代码中,我希望直接使用 p
而不是 p
的副本,
p[0] = 111
p[1:3] = [222, 333]
print(p) # [111, 222, 333]
感觉
del p[:]
与p[:]
一致,均引用原列表
但是
q=p[:]
在这种情况下,p[:]
会导致一个新列表!
我的新手期望是这样的
q=p[:]
应该和
一样q=p
为什么创作者允许这种特殊行为产生副本?
【问题讨论】:
因为历史原因。我同意,切片应该返回参考切片,而不是新列表。这就是它在numpy
中的工作方式,也是用户在大多数用例中想要的。事实上,python 添加了list.copy
方法是因为这种奇怪的混淆了人们(因为之前使用[:]
复制列表,而使用.copy()
复制字典和集合)。许多旧的 python 在方法上返回了低效的副本(参见dict.items
和zip
),这在 python 3 中得到了修复,但列表切片仍然作为副本。我想用[a:b].copy()
替换每个[a:b]
被认为太麻烦了
@FHTMitchell: list.copy
由于混淆而没有添加,添加它是为了使list
与其他集合类型更兼容(set
/dict
,必须有@ 987654344@ 因为切片对它们不起作用)。返回引用切片使得推理代码变得更加变得更加困难,并且更加更容易意外地执行诸如意外修改调用者参数之类的事情。 numpy
这样做是出于性能目的;一般来说,Python 更关心的是让编写正确的代码变得容易,而性能是次要的问题。
@FHTMitchell:不,这样做不是为了减少混乱,而是为了增加灵活性。他们的想法是,他们希望通过在收到的任何内容上调用 .copy()
来使函数能够避免类型复制公共集合。如果您需要序列,正确的解决方案仍然是使用seq[:]
,因为它可以处理可变和不可变序列。
如果你想复制一份列表,为了清晰起见,我推荐q = list(p)
而不是q = p[:]
。
@FHTMitchell:dict slice 有一些非常奇怪的行为,这使得它很难用于列表。一方面,如果你改变字典,字典视图将会中断。由于 dict 视图通常仅在迭代期间短期使用,并且由于 dicts 是无序的,因此在这种情况下它的问题要小得多,但是列表切片可以并且将被传递。此外,如果基础列表更改,不可变列表切片可能会意外更改。如果有人做了list.insert()
,列表视图应该如何表现?默认情况下,惰性切片有其吸引力,但它会使语言对初学者更难。
【参考方案1】:
del 和 assignments 的设计是一致的,只是它们的设计方式与您期望的方式不同。 del 从不删除对象,它删除名称/引用(对象删除只会间接发生,删除对象的是引用计数/垃圾收集器);同样,赋值运算符从不复制对象,它总是在创建/更新名称/引用。
del 和赋值运算符采用参考规范(类似于 C 中的左值概念,尽管细节有所不同)。此参考规范可以是变量名称(普通标识符)、__setitem__
键(方括号中的对象)或__setattr__
名称(点后的标识符)。此左值不会像表达式一样计算,因为这样做会导致无法分配或删除任何内容。
考虑以下之间的对称性:
p[:] = [1, 2, 3]
和
del p[:]
在这两种情况下,p[:]
的工作方式相同,因为它们都被评估为左值。另一方面,在下面的代码中,p[:]
是一个完全计算为对象的表达式:
q = p[:]
【讨论】:
有趣。所以,我想知道为什么 numpy 没有遵循相同的推理?p[:]
在 numpy 中类似但不同。有关视图,请参阅docs。 “切片数组返回它的视图”,视图是“查看相同数据的新数组对象”
@brainOverflow numpy 希望尽可能高效,并且尽可能简洁易读。既然 numpy 代码会被优化,为什么要让最常见的代码更冗长呢? numpy 不是作为一个通用框架诞生的,它是为具有一定专业知识水平的人设计的,因此他们做出的选择与 Guido 在设计 python 语言时做出的选择不同
标准python和numpy在赋值和删除操作上完全一样;它们的区别不在于赋值和删除,而在于在右值上下文中评估时的切片。一个 numpy 数组在切片时返回一个视图,标准 python 返回一个副本。这个微小的差异会对标准 python 和 numpy 的操作行为产生巨大影响;当标准 python 不覆盖时,numpy 将覆盖默认数据。但是,如果您了解 python 对象模型并且 numpy slices 是一个视图,那么这些行为差异是有道理的并且实际上非常简单。【参考方案2】:
迭代器上的del
只是以索引为参数调用__delitem__
。就像括号调用 [n] 是对索引为 n 的迭代器实例上的 __getitem__
方法的调用。
因此,当您调用 p[:]
时,您正在创建一个项目序列,而当您调用 del p[:]
时,您将 del/__delitem__ 映射到该序列中的每个项目。
【讨论】:
„del
on iterator“似乎是错误的,因为未定义迭代器上的del
。
他没有说“del on iterator”,他说的是“del on sequence”。即使它不正确的实现(你没有映射任何东西),它也是一种思考方式,通常会得出正确的结论。
@JaccovanDorp 答案字面意思以“del
on iterator is ...”开头,所以 ipaleka 确实说/写了。
对不起,我无法重构我之前的思路。【参考方案3】:
正如其他人所说; p[:]
删除p
中的所有项目;但不会影响 q。要更详细地了解列表docs,请参考以下内容:
所有切片操作都返回一个包含请求的新列表 元素。这意味着下面的切片返回一个新的(浅) 列表副本:
>>> squares = [1, 4, 9, 16, 25] ... >>> squares[:] [1, 4, 9, 16, 25]
所以q=p[:]
创建了p
的(浅) 副本作为单独的列表,但进一步检查它确实指向内存中完全独立的位置。
>>> p = [1,2,3]
>>> q=p[:]
>>> id(q)
139646232329032
>>> id(p)
139646232627080
这在copy
模块中有更好的解释:
浅拷贝构造一个新的复合对象,然后(到 尽可能)将引用插入其中找到的对象 原件。
虽然del 语句是在列表/切片上递归执行的:
删除一个目标列表递归删除每个目标,从左到右。
因此,如果我们使用del p[:]
,我们将通过迭代每个元素来删除p
的内容,而q
并没有如前所述进行更改,它引用了一个单独的列表,尽管具有相同的项目:
>>> del p[:]
>>> p
[]
>>> q
[1, 2, 3]
事实上,列表文档以及list.clear
方法中也引用了这一点:
list.copy()
返回列表的浅表副本。等效于
a[:]
。list.clear()
从列表中删除所有项目。相当于
del a[:]
。
【讨论】:
del
语句不会在该列表上递归执行。该文档讨论的是目标列表,而不是示例中的一个列表+切片。考虑del parrot, spam, grail
——这里的目标列表包含三个从左到右删除的元素,首先是parrot
,然后是spam
,然后是grail
。在del p[:]
中只有一个目标被删除:p[:]
。【参考方案4】:
基本上,切片语法可以在 3 种不同的上下文中使用:
访问,即x = foo[:]
设置,即foo[:] = x
删除,即del foo[:]
在这些上下文中,放在方括号中的值只是选择项目。这是为了在每种情况下都一致地使用“切片”:
所以x = foo[:]
获取foo
中的所有元素 并将它们分配给x
。这基本上是一个浅拷贝。
但foo[:] = x
会将foo
中的所有元素 替换为x
中的元素。
并且在删除del foo[:]
时会删除foo
中的所有元素。
但是,正如3.3.7. Emulating container types 所解释的,此行为是可自定义的:
object.__getitem__(self, key)
调用以实现
self[key]
的评估。对于序列类型,接受的键应该是整数和切片对象。请注意,负索引的特殊解释(如果类希望模拟序列类型)取决于__getitem__()
方法。如果 key 的类型不合适,TypeError
可能会被引发;如果序列的索引集之外的值(在对负值进行任何特殊解释之后),则应引发IndexError
。对于映射类型,如果缺少键(不在容器中),则应引发KeyError
。注意
for
循环期望为非法索引引发IndexError
,以便正确检测序列的结尾。
object.__setitem__(self, key, value)
调用以实现对
self[key]
的分配。与__getitem__()
相同的注释。如果对象支持更改键的值,或者可以添加新键,或者如果可以替换元素,则仅应为映射实现此功能。对于不正确的键值,应引发与__getitem__()
方法相同的异常。
object.__delitem__(self, key)
调用以实现删除
self[key]
。与__getitem__()
相同的注释。如果对象支持删除键,则仅应为映射实现此功能,如果可以从序列中删除元素,则应为序列实现。对于不正确的键值,应引发与__getitem__()
方法相同的异常。
(强调我的)
所以理论上任何容器类型都可以实现它,但它想要。然而,许多容器类型遵循列表实现。
【讨论】:
【参考方案5】:我不确定你是否想要这样的答案。换句话说,对于 p[:],它意味着“遍历 p 的所有元素”。如果你在
中使用它q=p[:]
那么它可以读作“迭代p的所有元素并将其设置为q”。另一方面,使用
q=p
只是意味着“将 p 的地址分配给 q”或“使 q 成为指向 p 的指针”,如果您来自其他单独处理指针的语言,这会令人困惑。
因此,在del中使用它,就像
del p[:]
只是表示“删除p的所有元素”。
希望这会有所帮助。
【讨论】:
【参考方案6】:主要是历史原因。
在 Python 的早期版本中,迭代器和生成器并不是真正的东西。大多数处理序列的方法只返回列表:例如,range()
返回包含数字的完整构造列表。
因此,当在表达式的右侧使用切片时,返回一个列表是有意义的。 a[i:j:s]
返回了一个新列表,其中包含来自 a
的选定元素。因此,赋值右侧的a[:]
将返回一个包含a
的所有元素的新列表,即浅拷贝:这在当时是完全一致的。
另一方面,表达式左侧侧的括号总是修改原始列表:这是a[i] = d
设置的先例,然后是del a[i]
,并且然后通过del a[i:j]
。
时间过去了,到处复制值和实例化新列表被认为是不必要且昂贵的。如今,range()
返回一个生成器,该生成器仅在请求时生成每个数字,并且迭代切片可能会以相同的方式工作 - 但 copy = original[:]
的习语作为历史产物已经根深蒂固。
顺便说一句,在 Numpy 中,情况并非如此:ref = original[:]
将进行引用而不是浅拷贝,这与 del
和分配给数组的工作方式是一致的。
>>> a = np.array([1,2,3,4])
>>> b = a[:]
>>> a[1] = 7
>>> b
array([1, 7, 3, 4])
Python 4,如果发生的话,可能会效仿。正如您所观察到的,它与其他行为更加一致。
【讨论】:
为切片创建副本仍然有意义。我不明白为什么 Python 4 应该效仿。 @BlackJack 迭代其他数据结构通常不会创建副本:例如,如果您想要字典键的单独副本,则需要使用list(d.keys())
。如果您必须对切片执行相同操作,这对我来说很有意义:list(a[:])
或只是 a.copy()
。
我不明白你的意思。遍历列表也不会创建副本。此外,如果original[:]
将创建一个视图,或者在 Numpy 中没有真正定义一个副本。只是如果你真的需要一份独立的副本,你必须明确要求一份。在 Numpy 中切片有时会给出一个视图,有时会给出一个副本。例如,在某些情况下,切片的切片不能再用偏移量、尺寸和步幅表示为视图。
这个“历史”原因在今天的 python 中仍然和当时一样重要。 numpy 数组之所以能返回视图,是因为 numpy 数组的维度是不可变的(数组不可调整大小)。 Python 的列表是完全可变的,切片返回默认值会使该语言更难正确用于常见用例,并且由于额外的间接性可能会更慢。以上是关于为啥 p[:] 在这两种情况下的工作方式不同?的主要内容,如果未能解决你的问题,请参考以下文章
为啥门控激活函数(在 Wavenet 中使用)比 ReLU 工作得更好?