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


140

我已经阅读了几本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是相同还是不同?


12
您正在注意可变性的错误方面:“ id是相同还是不同”只是一个副作用;“以前引用同一对象的其他引用所指向的数据现在是否反映更新”至关重要。
查尔斯·达菲

Answers:


124
  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

11
@musicfreak,请参阅我刚才所做的编辑,其中建立元组的速度比建立等效列表的速度快7.6倍-现在,您不能再说“从未见过明显的区别”,除非您对“明显”的定义“ 确实很奇特...
Alex Martelli

11
@musicfreak我认为您在滥用“过早的优化是万恶之源”。在应用程序中进行过早优化(例如,说“元组比列表快,因此我们将在所有应用程序中仅使用元组!”)与进行基准测试之间存在巨大差异。Alex的基准测试很有见地,并且知道构建元组比构建列表更快,这可能会帮助我们进行未来的优化操作(在真正需要时)。
维吉尔·杜普拉斯

5
@Alex,“构建”元组真的比“构建列表”快吗?还是我们看到Python运行时将元组缓存的结果?在我看来,后者。
三联画

6
@ACoolie,这完全由random调用控制(尝试这样做,您将看到!),所以不是很重要。尝试一下python -mtimeit -s "x=23" "[x,x]",您会发现构建元组和构建列表的速度提高了2-3倍。
Alex Martelli'2

9
对于任何想知道的人-通过从列表切换到元组,我们能够节省一个多小时的数据处理。
Mark Ribau

42

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

元组和列表有不同的用途。列表存储同类数据。您可以并且应该有这样的列表:

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

正确使用列表的原因是因为这些列表都是同类型的数据,尤其是人们的名字。但采取这样的清单:

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

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

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

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

请不要。


编辑:

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


13
我认为您的愿景至少没有得到我的同意,不认识其他人。
Stefano Borini'2

13
我也强烈不同意这个答案。数据的均匀性与应该使用列表还是元组绝对无关。Python中没有任何东西暗示这种区别。
Glenn Maynard'2

14

11
尽管Guido(Python的设计者)打算将列表用于同类数据,将元组用于异构数据库,但事实是该语言并未强制执行此操作。因此,我认为这种解释最主要的是风格问题。碰巧的是,在许多人的典型用例中,列表往往类似于数组,而元组往往类似于记录。但这并不能阻止人们使用列表来更好地解决他们的异构数据。正如Python的禅宗所说:实用胜过纯粹。
约翰Y 2010年

9
@格伦,你基本上错了。元组的主要用途之一是作为一种复合数据类型,用于存储多个相关数据。您可以遍历元组并执行许多相同的操作,这一事实并不能改变这一点。(作为参考,许多其他语言中的元组没有与列表中的元组相同的可迭代功能)
HS。

22

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

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

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

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

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

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

另请参见Dive Into Python中的介绍元组 ” 。


2
id((1,2,3))== id((1,2,3))为假。您不能仅通过比较位置来比较元组,因为不能保证它们是通过引用复制的。
Glenn Maynard'2

@Glenn:请注意限定词“在使用按引用复制时”。尽管编码人员可以创建自己的实现,但是元组的按引用复制在很大程度上是解释器/编译器的问题。我主要指的是如何==在平台级别实施。
2010年

1
@Glenn:还请注意按引用复制不适用于中的元组(1,2,3) == (1,2,3)。这更多是一个实习问题。
2010年

就像我说得很清楚,并不能保证它们被引用复制。元组没有在Python中实习;这是一个字符串概念。
Glenn Maynard'2

就像我说得很清楚:我不是在谈论程序员通过比较位置来比较元组。我说的是平台可以保证按引用复制的可能性。同样,interning可以应用于任何不可变的类型,而不仅仅是字符串。Python的主要实现不一定是不可变类型,但Python具有不可变类型使得事实成为了一种选择。
2010年

15

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

就其价值而言,最近的元组(2.6+)index()count()方法


5
+1:作为字典键的可变列表(或可变集合或可变字典)不起作用。因此,我们需要不可变的列表(“元组”),冻结集合以及……好吧,我想是冻结字典。
S.Lott

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 ++映射和集合中使用了很多次。


>>> a = [1,2,3] >>> id(a)3084599212L >>> a [1] = 5 >>> a [1,5,3] >>> id(a)3084599212L我刚刚修改了可变数据类型,因此与原始问题没有任何意义。X =“Hello”的ID(X)12345 X =‘再见’的ID(x)的65432谁在乎,如果它是一个新的对象或不只要X点,我已经分配的数据,这是最重要的。
pyNewGuy 2010年

4
您很困惑,我无法提供帮助。
Glenn Maynard'2

+1指出子问题中的混乱,这似乎是理解元组值的主要困难来源。
2010年

1
如果可以的话,另一个+1指出该键的真正含义是该对象是否可哈希docs.python.org/glossary.html#term-hashable)。
2010年

7

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

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

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

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

如果我可以更改元素的顺序,并且代码中没有问题(除了假设,例如“应该排序”),则应使用列表。如果不行(就像blue上面的元组一样),那么我应该使用一个元组。


如果可以的话,我将这个答案投票最多15次。这正是我对元组的感觉。
格兰特·保罗

6

它们很重要,因为它们可以保证调用者不会忽略传递给它们的对象。如果您这样做:

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

呼叫者无法保证呼叫后a的值。然而,

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

现在,您作为此代码的调用者或阅读者知道a是相同的。在这种情况下,您始终可以复制列表并通过该列表,但是现在您是在浪费时间,而不是使用更具语义意义的语言构造。


1
这是元组的非常次要的属性。在很多情况下,无论是预先存在的列表还是其他类,都有一个想要传递给函数但不希望对其进行修改的可变对象。在Python中没有“通过引用引用常量参数”的概念(例如const foo&在C ++中)。如果完全可以方便地使用元组,则元组恰好会为您提供此功能,但是如果您从调用方收到了一个列表,那么您真的要在将其传递到其他地方之前将其转换为元组吗?
Glenn Maynard'2

我同意你的这个观点。元组与对const关键字打耳光不同。我的观点是,元组的不变性为代码的读者带来了更多的意义。给定一种既可行又您期望的情况,使用元组将为读者增加额外的意义(同时
也要

a = [1,1,1] doWork(a)如果dowork()定义为def dowork(arg):arg = [0,0,0]在列表或元组上调用dowork()的结果相同
pyNewGuy


1

您的问题(和后续评论)集中在id()在分配期间是否发生变化。专注于不变对象替换和可变对象修改之间的差异的后续影响,而不是差异本身,也许不是最佳方法。

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

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

在这种情况下,即使仅为a1分配了新值,a2的内容也被更改。与以下内容对比:

>>> 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'}

使用元组,可以安全地防止字典的键“从其下方”更改为散列为不同值的项目。这对于有效执行至关重要。


正如其他人指出的,不变性=哈希性。并非所有元组都可以用作字典键:{([[1],[2]):'value'}失败,因为可以更改元组中的可变列表,但是{(((1),(2)):'值”}是好的。
内德·迪利

内德,是的,但是我不确定区别是否与所提问题有密切关系。
查尔斯·达菲

@ K.Nicholas,您在此处批准的编辑更改了代码,完全是分配了一个整数而不是一个元组-使得以后的索引操作失败,因此他们不可能测试新的成绩单实际上是可能的。正确识别问题,可以肯定;无效的解决方案。
查尔斯·达菲

@MichaelPuckettII同样,请参见上文。
查尔斯·达菲
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.