为什么p [:]设计在这两种情况下的工作方式有所不同


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

为什么创建者允许这种特殊的行为会导致副本?

del和作业的设计始终如一,它们只是没有设计您期望它们的方式。del永远不会删除对象,它删除名称/引用(对象删除仅间接发生,是删除对象的重新卡/垃圾收集器(;同样,分配运算符永远不会复制对象,它总是创建/更新名称/参考。

DEL和分配操作员采用参考规范(类似于C中的LVALUE的概念,尽管详细信息有所不同(。此参考规范是变量名(普通标识符(,__setitem__键(Square Bracket中的对象(或__setattr__名称(DOT之后的标识符(。此lvalue不会像表达式一样评估,因为这样做将使不可能分配或删除任何内容。

考虑:

之间的对称性
p[:] = [1, 2, 3]

del p[:]

在这两种情况下,p[:]都起作用,因为它们都被评估为LVALUE。另一方面,在以下代码中,p[:]是一个完全评估为对象的表达式:

q = p[:]

迭代器上的 del只是用索引作为参数的 __delitem__的调用。就像括号呼叫[n]是拨打Inderator实例上的__getitem__方法n。

的调用。

因此,当您调用p[:]时,您正在创建一系列项目,当您调用del p[:]时,您将del/__ delitem__映射到该序列中的每个项目。

正如其他人所说的;p[:]删除p中的所有项目;但不会影响Q。要进一步详细介绍列表文档,请参阅此信息:

所有切片操作返回包含所请求的新列表 元素。这意味着以下切片返回一个新的(浅( 列表的副本:

>>> 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方法:

列表。 copy((

返回列表的浅副本。等效于a[:]

列表。 clear((

从列表中删除所有项目。等效于del a[:]

基本上可以在3种不同的上下文中使用slice-syntax:

  • 访问,即x = foo[:]
  • 设置,即foo[:] = x
  • 删除,即del foo[:]

,在这些情况下,放入方括号中的值只需选择项目即可。这是设计为"切片"的。在每种情况下都始终使用:

  • 因此,x = foo[:]获取 foo中的所有元素并将其分配给x。这基本上是一个浅副本。

  • ,但是foo[:] = xfoo中使用x中的元素。

  • ,当删除 del foo[:]将删除时, foo中的所有元素。

但是,如3.3.7所述,这种行为是可以自定义的。模拟容器类型:

object.__getitem__(self, key)

呼叫以实现self[key] 的评估。对于序列类型,接受的键应为整数和切片对象。请注意,负索引的特殊解释(如果类希望模仿序列类型(取决于__getitem__()方法。如果键是不适当的类型,则可以提高TypeError;如果在序列的索引集之外的值(在对负值进行任何特殊解释之后(,则应提高IndexError。对于映射类型,如果丢失键(不在容器中(,则应提高KeyError

注意

for循环期望将IndexError用于非法索引以允许正确检测序列的末端。

object.__setitem__(self, key, value)

呼叫将分配实施到self[key] 。与__getitem__()相同的注释。仅当对象支持对键的值更改,或者可以添加新密钥时,或者如果可以更换元素,则应为映射实现这一点。对于不正确的键值,应提出相同的例外,与__getitem__()方法。

object.__delitem__(self, key)

呼叫以实现self[key] 的删除。与__getitem__()相同的说明。仅当对象支持去除密钥时,或在可以从序列中删除元素时,才应为映射实现。对于不正确的键值,应提出相同的例外,与__getitem__()方法。

(强调我的(

因此,从理论上讲,任何容器类型都可以按照需要实施。但是,许多容器类型都遵循列表 - 实践。

我不确定您是否想要这种答案。用文字来说,对于p [:],它的意思是"遍历P的所有元素"。如果您在

中使用它
q=p[:]

然后可以将其读为"迭代所有p的元素并将其设置为q"。另一方面,使用

q=p

只是意味着"将p的地址分配给q"或"使Q作为p"指针"将P"指向P",如果您来自其他可以单独处理指针的语言,这会造成混淆。

因此,在DEL中使用它,例如

del p[:]

仅表示"删除p的所有元素"。

希望这会有所帮助。

历史原因,主要是。

在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,如果发生过,可能会效仿。正如您所观察到的那样,它与其他行为更加一致。

最新更新