元组为什么可以包含可变项?


182

如果一个元组是不可变的,那么为什么它可以包含可变项呢?

这似乎是一个矛盾,当可变项(如列表)确实被修改时,它所属的元组保持不变。

Answers:


208

这是一个很好的问题。

关键的见解是,元组无法知道其中的对象是否可变。使对象可变的唯一方法是拥有更改其数据的方法。通常,无法检测到此情况。

另一个见解是Python的容器实际上不包含任何东西。相反,它们保留对其他对象的引用。同样,Python的变量与编译语言中的变量不同。相反,变量名只是名称空间字典中的键,它们与对应的对象相关联。Ned Batchhelder在他的博客文章中很好地解释了这一点。无论哪种方式,对象仅知道其引用计数。他们不知道这些引用是什么(变量,容器或Python内部函数)。

这两种见解共同解释了您的奥秘(为什么当基础列表更改时,“包含”列表的不可变元组似乎也会更改)。实际上,元组没有改变(它对其他对象的引用与以前相同)。元组无法更改(因为它没有变异方法)。当列表更改时,没有通知元组更改(该列表不知道它是由变量,元组还是其他列表引用)。

当我们讨论该主题时,还有一些其他想法可以帮助您完善关于什么是元组,它们如何工作以及其预期用途的思维模型:

  1. 元组的特征较少在于其不变性,而其特征在于其预期目的。
    元组是Python在一个屋檐下收集异构信息的一种方式。例如, s = ('www.python.org', 80) 将字符串和数字组合在一起,以便主机/端口对可以作为套接字(复合对象)传递。从这个角度来看,具有可变的组件是完全合理的。

  2. 不变性与另一个属性(哈希性)密切相关。但是哈希性不是绝对的属性。如果元组的组成部分之一不可散列,则整个元组也不可散列。例如,t = ('red', [10, 20, 30])不可散列。

最后一个示例显示了一个包含字符串和列表的2元组。元组本身是不可变的(即,它没有任何更改其内容的方法)。同样,字符串是不可变的,因为字符串没有任何突变方法。列表对象确实具有变异方法,因此可以对其进行更改。这表明可变性是对象类型的属性-有些对象具有突变方法,有些则没有。这并不会因为对象被嵌套而改变。

记住两件事。首先,不变性不是魔术,而是缺少突变方法。其次,对象不知道哪些变量或容器引用了它们-它们仅知道引用计数。

希望这对您有用:-)


这不是错误的“元组无法知道其中的对象是否可变”。我们可以检测到引用是否实现了哈希方法,那么它是不可变的。就像dic或set一样。这不是元组的设计决定吗?
garg10may

@ garg10may 1)不调用就不容易检测到哈希性,hash()因为从object()继承的所有内容都是可哈希的,因此子类需要显式关闭哈希。2)哈希不能保证不变性-易于创建可变的可哈希对象示例。3)与Python中的大多数容器一样,元组仅具有对基础对象的引用-它们没有义务检查它们并对其进行推断。
雷蒙德·赫汀格

173

这是因为元组包含列表,字符串或数字。它们包含对其他对象的引用1无法更改元组包含的引用的顺序并不意味着您不能对与这些引用关联的对象进行突变。2

1.对象,值和类型(请参阅:倒数第二段)
2 .标准类型层次结构(请参阅:“不可变序列”)


8
这个问题模棱两可。这个答案充分说明了为什么元组可能包含可变对象。它没有解释为什么元组被设计为包含可变对象。我认为后者是更相关的问题。
senderle 2012年

16

首先,“不变”一词对不同的人可能意味着许多不同的事物。我特别喜欢Eric Lippert在他的博客文章中对不变性的分类。在那里,他列出了这些不变性:

  • 真实性不变
  • 一次写入不变性
  • 冰棒不可变性
  • 浅与深不变性
  • 不变的外墙
  • 观察不变性

可以通过多种方式将它们组合起来,以实现更多种不变性,而且我敢肯定,还有更多种不变性。您似乎对深层(也称为传递)不变性感兴趣的一种不可变性,其中不可变对象只能包含其他不可变对象。

关键在于,深度不变性只是许多不变性中的一种。只要知道您的“不可变”概念可能与其他人的“不可变”概念不同,就可以采用您喜欢的任何一种。


Python元组具有什么样的不变性?
qazwsx 2012年

3
Python元组具有浅(也称为非传递性)不变性。
肯·韦恩·范德林德

16

据我所知,这个问题需要改写为关于设计决策的问题:Python的设计者为什么选择创建一个可以包含可变对象的不可变序列类型?

要回答这个问题,我们必须考虑的宗旨元组服务:他们作为快速通用序列。考虑到这一点,很明显为什么元组是不​​可变的却可以包含可变对象。以机智:

  1. 元组速度快且内存效率高:元组的创建是比列表更快的,因为它们是不可变的。不变性意味着可以使用常量折叠将元组创建为常量并按此方式加载。这也意味着由于不需要过度分配等原因,它们的创建速度更快,内存使用效率更高。它们比随机访问列表的速度慢一点,但是对于拆包又要更快(至少在我的机器上)。如果元组是可变的,那么它们就不会达到这样的目的。

  2. 元组是通用的:元组需要能够包含任何类型的对象。它们习惯于(快速地)执行可变长度参数列表之类的事情(通过*函数定义中的运算符)。如果元组不能容纳可变对象,那么它们对于这样的事情将毫无用处。Python必须使用列表,这可能会减慢速度,并且肯定会降低内存效率。

因此,您看到,为了实现其目的,元组必须是不可变的,而且还必须能够包含可变对象。如果Python的设计人员想要创建一个不可变的对象,以保证它“包含”的所有对象也是不可变的,那么他们将必须创建第三个序列类型。增益不值得额外的复杂性。


12

您不能更改id其项目。因此它将始终包含相同的项目。

$ python
>>> t = (1, [2, 3])
>>> id(t[1])
12371368
>>> t[1].append(4)
>>> id(t[1])
12371368

这是上面示例的最适当的演示。元组引用了那些不会改变的对象,尽管最多具有一个可变组件使整个元组无法散列。
piepi

5

我将在这里走出去的肢体和说,这里的相关部分是,虽然你可以改变一个列表的内容或对象的状态,包含一个元组中,你不能改变对象或列表在那里。如果您有一个依赖于事物[3]的列表,即使它为空,那么我会发现这很有用。


3

原因之一是Python中没有通用的方法将可变类型转换为不可变类型(请参见被拒绝的PEP 351以及有关为什么被拒绝的链接讨论)。因此,如果有此限制,就不可能将各种类型的对象放入元组,包括几乎所有用户创建的不可哈希对象。

字典和集合具有此限制的唯一原因是它们要求对象是可哈希的,因为它们在内部实现为哈希表。但是请注意,具有讽刺意味的是,字典和集合本身并不是不可变的(或不可哈希的)。元组不使用对象的哈希,因此其可变性无关紧要。


2

从元组本身不能扩展或收缩的意义上讲,元组是不可变的,并不是其中包含的所有项都是不可变的。否则,元组变钝。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.