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

Why do we need tuples in Python (or any immutable data type)?

我已经阅读了一些Python教程(比如说,深入了解Python),以及python.org上的语言参考——我不明白为什么该语言需要元组。

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

不变性?

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

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

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

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

1
2
3
4
5
6
>>> x='hello'
>>> id(x)
1234567
>>> x='good bye'
>>> id(x)
5432167

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


  • 不可变的对象可以允许实质性的优化;这大概是为什么字符串在Java中也是不可变的,它是单独开发的,但在Python的同时,几乎所有的东西在真正的函数语言中都是不可变的。

  • 尤其是在Python中,只有不可变的才是可哈希的(因此,集合的成员或字典中的键)。同样,这也提供了优化,但不仅仅是"实质性的"(设计一个体面的哈希表来存储完全可变的对象是一个噩梦——要么在散列之后立即复制所有内容,要么在最后一次引用对象后检查对象的哈希是否发生了变化的噩梦中抬起了丑陋的头)。

  • 优化问题示例:

    1
    2
    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


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

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

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

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

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

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

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

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

    请不要这样。

    编辑:

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


    if I must convert a tuple to a set or list to be able to sort them, what's the point of using a tuple in the first place?

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

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

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

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

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

    另请参见"介绍tuples",从dive into python开始。


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

    值得一提的是,最近tuples(2.6+)增长了index()count()方法。


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

    Why does anyone care if a variable lives at a different place in memory than when it was originally allocated? This whole business of immutability in Python seems to be over emphasized.

    这些是不同的东西。易变性与它存储在内存中的位置无关;它意味着它指向的东西是不能改变的。

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

    1
    2
    3
    4
    5
    6
    >>> x='hello'
    >>> id(x)
    1234567
    >>> x='good bye'
    >>> id(x)
    5432167

    这不是修改("变异")变量;而是创建一个同名的新变量,并丢弃旧变量。与突变操作相比:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> a = [1,2,3]
    >>> id(a)
    3084599212L
    >>> a[1] = 5
    >>> a
    [1, 5, 3]
    >>> id(a)
    3084599212L

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

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


    正如Gnibbler在评论中所说,Guido的观点没有被完全接受/认可:"列表是针对同质数据的,元组是针对异构数据的"。当然,许多反对者将这解释为列表中的所有元素都应属于同一类型。

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

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

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

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


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

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

    呼叫方不保证呼叫后的值。然而,

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

    现在,作为调用者或代码的读者,您知道a是相同的。对于这个场景,您可以一直复制列表并传递它,但是现在您正在浪费循环,而不是使用更具语义意义的语言构造。


    您的问题(以及后续注释)集中在id()是否在赋值期间更改。关注这个后续效应的不可变对象替换和可变对象修改之间的差异,而不是差异本身,可能不是最好的方法。

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

    1
    2
    3
    4
    5
    6
    7
    >>> a1 = [1]
    >>> a2 = a1
    >>> print a2[0]
    1
    >>> a1[0] = 2
    >>> print a2[0]
    2

    在这种情况下,A2的内容发生了变化,即使只有A1分配了一个新值。与以下内容形成对比:

    1
    2
    3
    4
    5
    6
    7
    >>> a1 = (1,)
    >>> a2 = a1
    >>> print a2[0]
    1
    >>> a1 = (2,)
    >>> print a2[0]
    1

    在后一种情况下,我们替换了整个列表,而不是更新其内容。对于不可变类型(如元组),这是唯一允许的行为。

    为什么这很重要?假设你有口述:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> 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'}

    使用元组,字典可以安全地将其键从"out-out-under-it"更改为哈希值不同的项。这对于实现有效性至关重要。


    你可以在这里看到一些关于这个的讨论