【问题标题】:Why do we need tuples in Python (or any immutable data type)?为什么我们需要 Python 中的元组(或任何不可变数据类型)?
【发布时间】:2011-01-11 13:27:20
【问题描述】:

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

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

不变性?

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

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

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

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

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

x还是引用了我想要的数据,为什么还要关心它的id是相同还是不同呢?

【问题讨论】:

  • 你注意到了可变性的错误方面:“id 是相同还是不同”只是一个副作用; “以前指向同一对象的其他引用指向的数据现在是否反映更新”很关键。

标签: python tuples


【解决方案1】:
  1. 不可变对象可以进行大量优化;这大概就是为什么字符串在 Java 中也是不可变的,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

【讨论】:

  • @musicfreak,查看我刚刚所做的编辑,其中构建元组比构建等效列表快 7.6 倍以上——现在你不能说你“从未见过明显的差异”更多,除非你对“引人注目”的定义是真正特别...
  • @musicfreak 我认为你在滥用“过早的优化是万恶之源”。在应用程序中进行过早优化(例如,说“元组比列表快,所以我们将在所有应用程序中只使用元组!”)和进行基准测试之间存在巨大差异。 Alex 的基准测试很有见地,并且知道构建元组比构建列表更快可能有助于我们未来的优化操作(当确实需要时)。
  • @Alex,“构建”元组真的比“构建列表”快,还是我们看到 Python 运行时缓存元组的结果?在我看来是后者。
  • @ACoolie,这完全由random 调用(尝试这样做,你会看到!),所以不是很重要。试试python -mtimeit -s "x=23" "[x,x]",你会发现构建元组与构建列表相比,速度提升了 2-3 倍。
  • 对于任何想知道的人 - 我们能够通过从列表切换到元组来减少一个多小时的数据处理。
【解决方案2】:

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

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

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

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

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

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

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

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

请不要。


编辑:

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

【讨论】:

  • 我认为你有一个至少我不同意的愿景,不知道其他人。
  • 我也非常不同意这个答案。数据的同质性​​与您应该使用列表还是元组完全无关。 Python 中没有任何东西表明这种区别。
  • Guido 几年前也提出了这一点。 aspn.activestate.com/ASPN/Mail/Message/python-list/1566320
  • 尽管 Guido(Python 的设计者)打算将列表用于同构数据和元组用于异构数据,但事实是该语言并未强制执行此操作。因此,我认为这种解释更多的是风格问题。碰巧在许多人的典型用例中,列表往往类似于数组,而元组往往类似于记录。但这不应该阻止人们使用列表来存储异构数据,如果它更适合他们的问题的话。正如 Python 之禅所说:实用胜于纯洁。
  • @Glenn,你基本上错了。元组的主要用途之一是作为一种复合数据类型,用于存储多条相关的数据。您可以迭代一个元组并执行许多相同的操作这一事实并没有改变这一点。 (作为参考,考虑到许多其他语言中的元组与列表对应项没有相同的可迭代特性)
【解决方案3】:

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

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

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

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

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

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

另见“Introducing tuples”,来自Dive Into Python

【讨论】:

  • id((1,2,3))==id((1,2,3)) 为假。您不能仅通过比较位置来比较元组,因为不能保证它们是通过引用复制的。
  • @Glenn:注意限定词“当您使用按引用复制时”。虽然编码人员可以创建自己的实现,但元组的引用复制在很大程度上是解释器/编译器的问题。我主要指的是== 在平台级别是如何实现的。
  • @Glenn: 另请注意,按引用复制不适用于(1,2,3) == (1,2,3) 中的元组。这更像是实习的问题。
  • 就像我说的很清楚,不能保证它们是通过引用复制的。元组没有在 Python 中实习。这是一个字符串的概念。
  • 就像我说得很清楚:我不是在谈论程序员通过比较位置来比较元组。我说的是平台可以保证按引用复制的可能性。此外,实习可以应用于任何不可变类型,而不仅仅是字符串。主要的 Python 实现可能不实习不可变类型,但 Python 具有不可变类型的事实使得实习成为一种选择。
【解决方案4】:

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

不管怎样,最近 (2.6+) 元组增长了 index()count() 方法

【讨论】:

  • +1:可变列表(或可变集或可变字典)作为字典键不起作用。所以我们需要不可变的列表(“元组”)、冻结的集合,以及……好吧……我想是一个冻结的字典。
【解决方案5】:

我一直发现为相同的基本数据结构(数组)设置两种完全不同的类型是一种尴尬的设计,但在实践中并不是一个真正的问题。 (每种语言都有其缺陷,包括 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="goodbye" id(x) 65432 谁在乎它是否是一个新对象。只要 x 指向我分配的数据,这就是最重要的。
  • 你的困惑远远超出了我的能力范围。
  • +1 用于指出子问题中的混淆,这似乎是难以理解元组值的主要来源。
  • 如果可以的话,另一个 +1 指出键的真正标准是对象是否可散列 (docs.python.org/glossary.html#term-hashable)。
【解决方案6】:

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

我喜欢以不同的方式看待它,不像others过去也有:

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

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

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

【讨论】:

  • 如果可以的话,我会给这个答案投票 15 次。这正是我对元组的感受。
【解决方案7】:

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

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

调用者无法保证调用后a的值。 不过,

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

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

【讨论】:

  • 这是元组的一个非常次要的属性。在很多情况下,您想要将一个可变对象传递给一个函数而不对其进行修改,无论它是一个预先存在的列表还是其他一些类。 Python 中没有“const 参数引用”的概念(例如 C++ 中的 const foo &)。如果碰巧使用元组很方便,元组恰好会给你这个,但是如果你从调用者那里收到了一个列表,你真的要在将它传递给其他地方之前将它转换为一个元组吗?
  • 我同意你的看法。元组与拍打 const 关键字不同。我的观点是,元组的不变性为代码的读者带来了额外的意义。鉴于两者都可以工作并且您的期望是它不应该使用元组更改将为读者增加额外的意义(同时确保它也是如此)
  • a = [1,1,1] doWork(a) 如果 dowork() 定义为 def dowork(arg): arg=[0,0,0] 在列表上调用 dowork()或元组有相同的结果
【解决方案8】:

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

【讨论】:

    【解决方案9】:

    您的问题(以及后续 cmets)关注 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'}
    

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

    【讨论】:

    • 正如其他人指出的那样,不变性!=哈希性。并非所有元组都可以用作字典键: { ([1], [2]) : 'value' } 失败,因为元组中的可变列表可以更改,但是 { ((1), (2)) : ' value' } 就可以了。
    • 内德,这是真的,但我不确定这种区别是否与所提出的问题密切相关。
    • @K.Nicholas,您在此处批准的编辑更改了代码,以便分配一个整数,而不是一个元组,这使得后面的索引操作失败,所以他们不能'不可能测试过新的成绩单实际上是可能的。正确识别的问题,当然;无效的解决方案。
    • @MichaelPuckettII,同样,见上文。
    猜你喜欢
    • 2014-06-30
    • 2015-10-23
    • 2013-10-18
    • 2020-12-17
    • 1970-01-01
    • 2015-06-05
    • 2016-08-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多