为什么我们需要在 Python 中使用元组(或任何不可变的数据类型) ?

我已经阅读了几个 Python 教程(其中之一是深入 Python) ,以及 Python.org 上的语言参考——我不明白为什么该语言需要元组。

元组与列表或集合相比没有方法,如果我必须将元组转换为集合或列表才能对它们进行排序,那么首先使用元组有什么意义呢?

永恒?

为什么有人关心变量在内存中的位置是否与它最初被分配时的位置不同呢?Python 中的这种不可变性似乎被过分强调了。

在 C/C + + 中,如果我分配一个指针并指向一些有效的内存,我不在乎地址位于哪里,只要在我使用它之前它不为空。

无论何时引用该变量,我都不需要知道指针是否仍然指向原始地址。我只是检查 null 并使用它(或不使用它)。

在 Python 中,当我分配一个字符串(或元组)赋值给 x,然后修改字符串时,为什么要关心它是否是原始对象呢?只要这个变量指向我的数据,那就够了。

>>> x='hello'
>>> id(x)
1234567
>>> x='good bye'
>>> id(x)
5432167

x仍然引用我想要的数据,为什么有人需要关心它的 id 是否相同或不同?

47660 次浏览

有时我们喜欢使用对象作为字典键

值得一提的是,元组最近(2.6 +)增加了 index()count()方法

你可以看到 给你对此进行一些讨论

  1. 不可变对象可以实现实质性的优化; 这大概就是为什么字符串在 Java 中也是不可变的,它与 Python 开发时间相当独立,而且几乎所有东西在真正的函数式语言中都是不可变的。

  2. 特别是在 Python 中,只有不可变的元素可以是散列的(因此,字典中的集合或键的成员也可以是散列的)。同样,这提供了优化,但远远不只是“实质性的”(设计像样的散列表来存储完全可变的对象是一场噩梦——要么你在散列后立即复制所有对象,要么检查对象的散列是否已经改变,因为你最后一次引用它是它丑陋的头)。

优化问题的例子:

$ python -mtimeit '["fee", "fie", "fo", "fum"]'
1000000 loops, best of 3: 0.432 usec per loop
$ python -mtimeit '("fee", "fie", "fo", "fum")'
10000000 loops, best of 3: 0.0563 usec per loop

如果我必须将一个元组转换为一个集合或列表才能对它们进行排序,那么首先使用一个元组有什么意义呢?

在这种特殊情况下,可能没有什么意义。这不是问题,因为在这种情况下您不会考虑使用元组。

正如您所指出的,元组是不可变的。拥有不可变类型的原因适用于元组:

  • 复制效率: 不需要复制不可变物件,你可以把它改成别名(将一个变量绑定到一个引用)
  • 比较效率: 当使用引用复制时,可以通过比较位置而不是内容来比较两个变量
  • 实习: 您最多需要存储任何不可变值的一个副本
  • 不需要在并发代码中同步对不可变对象的访问
  • 常量正确性: 一些值不应该被允许改变。这(对我来说)是不可变类型的主要原因。

注意,特定的 Python 实现可能不会使用上述所有特性。

字典键必须是不可变的,否则更改键对象的属性会使基础数据结构的不变量失效。因此元组可以潜在地用作键。这是常量正确性的结果。

另见“ 引入元组”,来自 潜入 Python

上面的答案都没有指出元组与列表的真正问题,许多 Python 新手似乎并不完全理解这个问题。

元组和列表有不同的用途。列表存储同质数据。你可以也应该有一个这样的列表:

["Bob", "Joe", "John", "Sam"]

正确使用列表的原因是因为这些都是同质类型的数据,特别是人名。但是列一个这样的清单:

["Billy", "Bob", "Joe", 42]

那个名单是一个人的全名和年龄。这不是一种数据。存储该信息的正确方法是在元组或对象中。假设我们有一些:

[("Billy", "Bob", "Joe", 42), ("Robert", "", "Smith", 31)]

Tuples 和名单的不变性和可变性并不是主要区别。列表是同类项目的列表: 文件、名称、对象。元组是不同类型对象的分组。它们有不同的用途,许多 Python 程序员滥用列表来表示元组的用途。

请不要这样。


编辑:

我认为这篇博客文章解释了为什么我认为这比我做的更好:

它们很重要,因为它们保证调用者所传递的对象不会发生变异。 如果你这样做:

a = [1,1,1]
doWork(a)

调用方不能保证调用后 的值。 但是,

a = (1,1,1)
doWorK(a)

现在,作为这段代码的调用者或读者,您知道 是相同的。 在这种情况下,您总是可以复制一个列表并传递它,但是现在您正在浪费周期,而不是使用语言结构,使其具有更多的语义意义。

我总是发现,对于相同的基本数据结构(数组)使用两种完全不同的类型是一种笨拙的设计,但在实践中却不是一个真正的问题。(每种语言都有它的缺点,包括 Python,但这不是一个重要的缺点。)

为什么有人关心变量在内存中的位置是否与它最初被分配时的位置不同呢?Python 中的这种不可变性似乎被过分强调了。

这是两码事。可变性与它存储在内存中的位置无关; 它意味着 指向的东西不能更改。

Python 对象在创建后不能更改位置,不管是否可变。(更准确地说,id ()的值不能改变——实际上也是一样。)可变对象的内部存储可以更改,但这是一个隐藏的实现细节。

>>> x='hello'
>>> id(x)
1234567
>>> x='good bye'
>>> id(x)
5432167

这并不是修改(“变异”)变量,而是创建一个具有相同名称的新变量,并放弃旧变量。与变异操作相比:

>>> a = [1,2,3]
>>> id(a)
3084599212L
>>> a[1] = 5
>>> a
[1, 5, 3]
>>> id(a)
3084599212L

正如其他人指出的那样,这允许使用数组作为字典的键,以及其他需要不可变性的数据结构。

注意,字典的键不一定是完全不可变的。只有用作键的部分需要是不可变的; 对于某些用法,这是一个重要的区别。例如,您可以有一个表示用户的类,它通过唯一的用户名比较相等性和散列。然后您可以将其他可变数据挂起在类上——“ user is login in”等等。由于这不会影响相等性或散列,因此可以在字典中将其作为键使用,并且这种做法是完全有效的。这在 Python 中并不常见; 我只是指出这一点,因为有几个人声称键需要是“不可变的”,这只是部分正确。不过,我在 C + + 映射和集合中已经使用过很多次了。

您的问题(以及后续评论)主要集中在 id ()是否在赋值过程中发生了变化。关注不可变物件替换和可变对象修改之间的差异的后续效应,而不是差异本身,可能不是最好的方法。

在我们继续之前,请确保下面演示的行为符合您对 Python 的期望。

>>> a1 = [1]
>>> a2 = a1
>>> print a2[0]
1
>>> a1[0] = 2
>>> print a2[0]
2

在本例中,a2的内容发生了更改,尽管只有 a1被赋了一个新值。与下列情况形成对比:

>>> a1 = (1,)
>>> a2 = a1
>>> print a2[0]
1
>>> a1 = (2,)
>>> print a2[0]
1

在后一种情况下,我们替换了整个列表,而不是更新其内容

为什么这很重要? 让我们假设你有一个结论:

>>> t1 = (1,2)
>>> d1 = { t1 : 'three' }
>>> print d1
{(1,2): 'three'}
>>> t1[0] = 0  ## results in a TypeError, as tuples cannot be modified
>>> t1 = (2,3) ## creates a new tuple, does not modify the old one
>>> print d1   ## as seen here, the dict is still intact
{(1,2): 'three'}

使用元组,字典可以安全地将其键“从下面”更改为散列为不同值的项。这对于有效实现是至关重要的。

正如在评论中提到的 nibbler,Guido 有一个不被完全接受/欣赏的 意见: “列表用于同质数据,元组用于异质数据”。当然,许多反对者认为这意味着列表中的所有元素都应该是同一类型的。

我希望看到它的不同之处,不像 其他人过去也有:

blue= 0, 0, 255
alist= ["red", "green", blue]

注意,我认为 alist 是同质的,即使 type (alist [1]) ! = type (alist [2])。

如果我可以改变元素的顺序,并且我的代码中不会出现问题(除了假设,例如“它应该被排序”) ,那么应该使用一个列表。如果不是(如上面的 tuple blue) ,那么我应该使用 tuple。