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

时间:2010-02-01 01:08:16

标签: python tuples

我已经阅读了几个python教程(Dive Into Python,一个),以及Python.org上的语言参考 - 我不明白为什么语言需要元组。

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

不变性?

为什么有人关心变量是否存在于内存中的不同位置而不是最初分配的位置?这整个Python的不变性业务似乎过分强调。

在C / C ++中,如果我分配一个指针并指向一些有效的内存,我不关心地址的位置,只要在我使用它之前它不是null。

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

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

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

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

9 个答案:

答案 0 :(得分:117)

  1. 不可变对象可以进行大量优化;这可能就是为什么字符串在Java中也是不可变的,它是单独开发的,但与Python几乎同时开发,几乎所有东西都是真正的函数式语言中不可变的。

  2. 特别是在Python中的
  3. ,只有不可变的可以是可散列的(因此,集合的成员或字典中的键)。同样,这提供了优化,但远远不仅仅是“实质性”(设计存储完全可变对象的合适哈希表是一场噩梦 - 要么你在哈希它时立即复制所有东西,或者检查对象的哈希是否是噩梦自从你上次参考它以后它变得丑陋的头脑已经改变了。

  4. 优化问题示例:

    $ 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
    

答案 1 :(得分:41)

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

元组和列表用于不同的目的。列表存储同质数据。您可以而且应该有这样的列表:

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

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

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

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

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

元组和列表的不变性和可变性不是主要区别。列表是相同类型的项目列表:文件,名称,对象。元组是不同类型对象的分组。它们有不同的用途,许多Python编码器滥用列表来表示元组的用途。

请不要。


编辑:

我认为这篇博文解释了为什么我认为这比我更好:http://news.e-scribe.com/397

答案 2 :(得分:22)

  

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

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

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

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

请注意,特定的Python实现可能无法使用上述所有功能。

字典键必须是不可变的,否则更改键对象的属性可能会使底层数据结构的不变量无效。因此,元组可以用作键。这是const正确性的结果。

另请参阅Introducing tuples中的“Dive Into Python”。

答案 3 :(得分:15)

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

对于它的价值,最近元组(2.6+)增长了index()count()方法

答案 4 :(得分:9)

我总是发现对于相同的基本数据结构(数组)有两个完全独立的类型是一个笨拙的设计,但在实践中不是一个真正的问题。 (每种语言都有瑕疵,包括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

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

请注意,词典的键不必完全不可变。只有用作密钥的部分需要是不可变的;对于某些用途,这是一个重要的区别。例如,您可以拥有一个表示用户的类,该类通过唯一的用户名比较相等性和哈希值。然后,您可以在类上挂起其他可变数据 - “用户已登录”等。由于这不会影响相等性或散列,因此将其用作字典中的键是可能且完全有效的。这在Python中并不常见;我只是指出它,因为有几个人声称密钥需要“不可变”,这只是部分正确。不过,我已经多次使用C ++地图和集合。

答案 5 :(得分:7)

正如gnibbler在评论中提到的那样,Guido有一个opinion未被完全接受/赞赏:“列表用于同类数据,元组用于异构数据”。当然,许多反对者将此解释为意味着列表中的所有元素应该属于同一类型。

我喜欢以不同的方式看待它,与others过去一样:

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

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

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

答案 6 :(得分:6)

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

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

来电者在通话后无法保证 a 的价值。 但是,

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

现在您作为此代码的来电者或读者都知道 a 是相同的。 您可以随时为此场景制作列表的副本并传递它,但现在您正在浪费周期而不是使用更具语义意义的语言构造。

答案 7 :(得分:1)

您可以看到here对此进行讨论

答案 8 :(得分:1)

您的问题(和后续评论)关注的是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'}

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