为什么p [:]设计为在这两种情况下工作不同?

时间:2019-06-26 23:33:02

标签: python python-3.x slice

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

为什么创建者允许这种特殊行为来代替副本?

6 个答案:

答案 0 :(得分:54)

del和分配的设计是一致的,只是没有按照您期望的方式设计它们。 del从不删除对象,它删除名称/引用(对象删除仅是间接发生的,是refcount /垃圾收集器删除对象);同样,赋值运算符从不复制对象,它总是在创建/更新名称/引用。

del和赋值运算符采用参考规范(类似于C中的左值的概念,尽管细节有所不同)。该参考规范可以是变量名(普通标识符),__setitem__键(方括号中的对象)或__setattr__名称(点号后的标识符)。该左值不像表达式那样求值,否则将无法分配或删除任何内容。

考虑以下两者之间的对称性:

p[:] = [1, 2, 3]

del p[:]

在两种情况下,p[:]的工作方式相同,因为它们都被视为左值。另一方面,在下面的代码中,p[:]是一个完全评估为对象的表达式:

q = p[:]

答案 1 :(得分:23)

迭代器上的

>>> a = np.array([1,2,3,4]) >>> b = a[:] >>> a[1] = 7 >>> b array([1, 7, 3, 4]) 只是对以索引为参数的del的调用。就像括号调用[n]是对索引为n的迭代器实例上的__delitem__方法的调用一样。

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

答案 2 :(得分:7)

正如其他人所说; 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方法中也引用了此方法:

  

列表。 copy()

     

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

     

列表。 clear()

     

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

答案 3 :(得分:6)

基本上,分片语法可以在3种不同的上下文中使用:

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

在这些情况下,放在方括号中的值只是选择项目。旨在在以下每种情况下一致使用“切片”:

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

  • 但是foo[:] = xfoo中的所有元素替换为x中的元素。

  • ,并且在删除del foo[:]时将删除foo中的所有元素

但是,此行为是可自定义的,如3.3.7. Emulating container types所述:

  

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__()方法相同的例外。

(强调我的)

因此,从理论上讲,任何容器类型都可以实现所需的实现。但是,许多容器类型都遵循列表实现。

答案 4 :(得分:2)

我不确定您是否需要这种答案。换句话说,对于p [:],它意味着“遍历p的所有元素”。如果在

中使用它
(import 'java.time.format.DateTimeFormatter)
(import 'java.time.temporal.TemporalQuery)
(import 'java.time.LocalDate)

(let [dtf (DateTimeFormatter/ISO_LOCAL_DATE)]
  (.parse dtf "2019-04-04"
          (reify TemporalQuery
                 (queryFrom [this temporal]
                            (LocalDate/from temporal)))))

然后可以将其理解为“使用p的所有元素进行迭代并将其设置为q”。另一方面,使用

q=p[:]

仅仅意味着“将p的地址分配给q”或“使q成为p的指针”,如果您来自单独处理指针的其他语言,这会造成混淆。

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

q=p

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

希望这会有所帮助。

答案 5 :(得分:2)

主要是历史原因。

在Python的早期版本中,迭代器和生成器并不是真正的东西。使用序列的大多数方法只是返回列表:例如,range()返回包含数字的完全构造的列表。

因此,在表达式的右侧使用切片时,返回列表是有意义的。 a[i:j:s]返回了一个新列表,其中包含来自a的选定元素。因此,在任务右侧的a[:]将返回一个包含a的所有元素的新列表,即浅表副本:这在当时是完全一致的。

另一方面,表达式的侧的括号始终会修改原始列表:这是a[i] = d设置的先例,而该先例后面是{{1 }},然后按del a[i]

时间流逝,在各处复制值和实例化新列表被认为是不必要且昂贵的。如今,del a[i:j]返回一个生成器,该生成器仅按要求生成每个数字,并且在切片上进行迭代可能以相同的方式工作,但是range()的习语已被根深蒂固地视为历史产物。

顺便说一句,在Numpy中,情况并非如此:copy = original[:]将进行引用而不是浅表副本,这与ref = original[:]和数组赋值的工作方式是一致的。

del

Python 4(如果曾经发生过)可能会效仿。正如您所观察到的,它与其他行为更加一致。