关于python:不可变类型与可变类型

Immutable vs Mutable types

我对什么是不变类型感到困惑。我知道float对象被认为是不可变的,这类例子来自我的书:

1
2
3
class RoundFloat(float):
    def __new__(cls, val):
        return float.__new__(cls, round(val, 2))

由于类结构/层次结构,这是否被认为是不可变的?,表示float位于类的顶部,是它自己的方法调用。类似于这类例子(尽管我的书中说dict是可变的):

1
2
3
class SortedKeyDict(dict):
    def __new__(cls, val):
        return dict.__new__(cls, val.clear())

虽然可变的东西在类内有方法,但是使用这种类型的示例:

1
2
3
class SortedKeyDict_a(dict):
    def example(self):
        return self.keys()

另外,对于最后一个class(SortedKeyDict_a),如果我将这种类型的集合传递给它:

1
d = (('zheng-cai', 67), ('hui-jun', 68),('xin-yi', 2))

不调用example方法,返回字典。带有__new__SortedKeyDict将其标记为错误。我试着用__new__将整数传递给RoundFloat类,它没有显示错误。


什么?浮点数是不变的?但我不能这样做

1
2
3
x = 5.0
x += 7.0
print x # 12.0

那不是"mut"x吗?

你同意字符串是不变的,对吗?但你也可以做同样的事情。

1
2
3
s = 'foo'
s += 'bar'
print s # foobar

变量的值会改变,但它会通过改变变量所指的内容而改变。一个可变的类型可以这样改变,它也可以"就地"改变。

这就是区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
x = something # immutable type
print x
func(x)
print x # prints the same thing

x = something # mutable type
print x
func(x)
print x # might print something different

x = something # immutable type
y = x
print x
# some statement that operates on y
print x # prints the same thing

x = something # mutable type
y = x
print x
# some statement that operates on y
print x # might print something different

具体实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
x = 'foo'
y = x
print x # foo
y += 'bar'
print x # foo

x = [1, 2, 3]
y = x
print x # [1, 2, 3]
y += [3, 2, 1]
print x # [1, 2, 3, 3, 2, 1]

def func(val):
    val += 'bar'

x = 'foo'
print x # foo
func(x)
print x # foo

def func(val):
    val += [3, 2, 1]

x = [1, 2, 3]
print x # [1, 2, 3]
func(x)
print x # [1, 2, 3, 3, 2, 1]


您必须理解,python将其所有数据表示为对象。其中一些对象(如列表和字典)是可变的,这意味着您可以在不更改其标识的情况下更改其内容。其他对象如整数、浮点数、字符串和元组是不能更改的对象。理解这一点的一个简单方法是查看对象ID。

下面是一个不可变的字符串。您不能更改其内容。如果你试图改变它,它将提高一个TypeError。另外,如果我们分配新的内容,将创建一个新的对象,而不是要修改的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> s ="abc"
>>>id(s)
4702124
>>> s[0]
'a'
>>> s[0] ="o"
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> s ="xyz"
>>>id(s)
4800100
>>> s +="uvw"
>>>id(s)
4800500

你可以用一个列表来做,它不会改变对象的标识。

1
2
3
4
5
6
7
8
>>> i = [1,2,3]
>>>id(i)
2146718700
>>> i[0]
1
>>> i[0] = 7
>>> id(i)
2146718700

要了解更多关于Python数据模型的信息,您可以查看Python语言参考:

  • python 2数据模型
  • python 3数据模型


通用不可变类型:

  • 编号:int()float()complex()
  • 不可变序列:str()tuple()frozenset()bytes()
  • 常见的可变类型(几乎所有其他类型):

  • 可变序列:list()bytearray()
  • 机组类型:set()
  • 映射类型:dict()
  • 类,类实例
  • 等。
  • 快速测试类型是否可变的一个技巧是使用id()内置函数。

    示例,使用整数,

    1
    2
    3
    4
    5
    6
    7
    8
    >>> i = 1
    >>> id(i)
    ***704
    >>> i += 1
    >>> i
    2
    >>> id(i)
    ***736 (different from ***704)

    使用列表,

    1
    2
    3
    4
    5
    6
    7
    8
    >>> a = [1]
    >>> id(a)
    ***416
    >>> a.append(2)
    >>> a
    [1, 2]
    >>> id(a)
    ***416 (same with the above id)


    首先,一个类是否有方法或者它的类结构与可变性无关。

    ints和floats是不变的。如果我这样做

    1
    2
    a = 1
    a += 5

    它将名称a指向位于第一行内存中某个位置的1。在第二行,它查找到1,加上5,得到6,然后把a指向记忆中的6,它没有以任何方式将1改为6。使用其他不可变类型,同样的逻辑适用于以下示例:

    1
    2
    3
    4
    b = 'some string'
    b += 'some other string'
    c = ('some', 'tuple')
    c += ('some', 'other', 'tuple')

    对于可变类型,我可以做一些实际更改存储在内存中的值的事情。用:

    1
    d = [1, 2, 3]

    我在内存中创建了123的位置列表。如果我那么做

    1
    e = d

    我只是把e指向同一个listd点。我可以这样做:

    1
    e += [4, 5]

    同时更新ed点所在的列表,使其在内存中也有45的位置。

    如果我回到一个不变的类型,用一个tuple做这个:

    1
    2
    3
    f = (1, 2, 3)
    g = f
    g += (4, 5)

    那么,f仍然只指向原来的tuple——你已经把g指向了一个全新的tuple

    现在,举个例子

    1
    2
    3
    class SortedKeyDict(dict):
        def __new__(cls, val):
            return dict.__new__(cls, val.clear())

    你经过的地方

    1
    d = (('zheng-cai', 67), ('hui-jun', 68),('xin-yi', 2))

    (这是tuplestupleval一样,因为tuple没有.clear()方法,所以你会得到一个错误——你必须把dict(d)作为val传递给它才能工作,在这种情况下,你会得到一个空的SortedKeyDict


    对象是否可变取决于其类型。这不取决于它是否有某些方法,也不取决于类层次结构的结构。

    用户定义的类型(即类)通常是可变的。有一些异常,例如不可变类型的简单子类。其他不可变类型包括一些内置类型,如intfloattuplestr,以及一些在C中实现的python类。

    python语言参考中"数据模型"一章的一般解释:

    The value of some objects can change. Objects whose value can change
    are said to be mutable; objects whose value is unchangeable once they
    are created are called immutable.

    (The value of an immutable container
    object that contains a reference to a mutable object can change when
    the latter’s value is changed; however the container is still
    considered immutable, because the collection of objects it contains
    cannot be changed. So, immutability is not strictly the same as having
    an unchangeable value, it is more subtle.)

    An object’s mutability is
    determined by its type; for instance, numbers, strings and tuples are
    immutable, while dictionaries and lists are mutable.


    如果您是从另一种语言(除了一种非常像python的语言,比如ruby)来到python,并且坚持用另一种语言来理解它,那么人们通常会在这里感到困惑:

    1
    2
    >>> a = 1
    >>> a = 2 # I thought int was immutable, but I just changed it?!

    在python中,赋值不是python中的突变。

    在C++中,如果你写a = 2,你会调用EDCOX1×1 },这会改变存储在EDCOX1×2中的对象。(如果a中没有存储对象,那就是错误。)

    在python中,a = 2a中存储的内容没有任何作用;它只是意味着2现在存储在a中。(如果a中没有存放物品,那就好了。)

    最终,这是一个更深层次的区别的一部分。

    像C++这样的语言中的变量是内存中的一个类型化的位置。如果a是一个int,这意味着编译器知道的4个字节应该被解释为int。所以,当您执行a = 2操作时,它会将存储在这4个字节内存中的内容从0, 0, 0, 1更改为0, 0, 0, 2。如果在其他地方还有另一个int变量,它有自己的4个字节。

    像python这样的语言中的变量是一个拥有自己生命的对象的名称。有一个对象用于数字1,另一个对象用于数字2a不是表示为int的4字节内存,它只是指向1对象的名称。把数字1转换成数字2对a = 2是没有意义的(这会给任何一个python程序员太多的能力来改变宇宙的基本工作);相反,它所做的只是让a忘记1对象并指向2对象。

    所以,如果任务不是突变,那么什么是突变?

    • 调用一个记录为突变的方法,比如a.append(b)。(注意,这些方法几乎总是返回None)。不可变类型没有任何此类方法,可变类型通常有。
    • 分配给对象的一部分,如a.spam = ba[0] = b。不可变类型不允许为属性或元素赋值,可变类型通常允许一个或另一个。
    • 有时使用增强赋值,如a += b,有时不使用。可变类型通常会改变值;不可变类型永远不会改变,而是给您一个副本(它们计算a + b,然后将结果赋给a)。

    但是,如果赋值不是突变,那么如何赋值给对象的一部分突变呢?这就是问题的症结所在。a[0] = b不突变EDCX1〔32〕(同样,与C++不同),但它确实变异EDCX1〔2〕(与C++不同,除了间接地)。

    所有这一切都是为什么最好不要把Python的语义放在一种你习惯的语言上,而是用它们自己的术语来学习Python的语义。


    可变对象必须至少有一个方法能够改变对象。例如,list对象有append方法,它实际上会改变对象:

    1
    2
    3
    4
    >>> a = [1,2,3]
    >>> a.append('hello') # `a` has mutated but is still the same object
    >>> a
    [1, 2, 3, 'hello']

    但是类float没有改变float对象的方法。你可以做到:

    1
    2
    3
    4
    >>> b = 5.0
    >>> b = b + 0.1
    >>> b
    5.1

    =操作数不是一种方法。它只是在变量和它右边的任何对象之间建立一个绑定,其他的都没有。它从不更改或创建对象。它是一个变量指向什么的声明,从现在开始。

    当执行b = b + 0.1操作时,=操作数将变量绑定到一个新的浮点,并使用5 + 0.1的结果创建wich。

    当您将一个变量赋给一个现存的对象时,无论变量是否可变,=操作数都会将该变量绑定到该对象。什么都没有发生

    在这两种情况下,=只是做了约束。它不会更改或创建对象。

    当执行a = 1.0操作时,=操作数不是用来创建浮点的,而是行的1.0部分。实际上,在编写1.0时,它是float(1.0)的简写,是一个返回float对象的构造函数调用。(这就是为什么如果键入1.0并按Enter键,您会得到下面打印的"echo"1.0;这是您调用的构造函数函数的返回值)

    现在,如果b是一个浮点数,而你给a = b赋值,这两个变量都指向同一个对象,但实际上变量之间不能相互通信,因为对象是不可变的;如果你给b += 1赋值,现在b指向一个新对象,a仍然指向旧对象,并且不知道b所指的。

    但是如果c是,比如说,list,而你指定a = c,那么现在ac可以"通信",因为list是可变的,如果你指定c.append('msg'),那么只要检查a就可以得到消息。

    (顺便说一句,每个对象都有一个唯一的ID号,它与id(x)有关。因此,您可以检查对象是否相同,或者不检查其唯一ID是否已更改。)


    可变对象和不可变对象的区别定义

    可变对象:创建后可以更改的对象。不可变对象:创建后不能更改的对象。

    在python中,将尝试更改它将给新对象的不可变对象的值。

    可变物件

    下面是python中可变类型的列表对象:

  • list
  • Dictionary
  • Set
  • bytearray
  • user defined classes
  • 不可变对象

    以下是Python中不可变类型的列表对象:

  • int
  • float
  • decimal
  • complex
  • bool
  • string
  • tuple
  • range
  • frozenset
  • bytes
  • 一些悬而未决的问题

    问题:字符串是不可变类型吗?答:是的,但你能解释一下吗?证据1:

    1
    2
    3
    a ="Hello"
    a +=" World"
    print a

    产量

    1
    "Hello World"

    在上面的示例中,字符串一度被创建为"hello",最后改为"hello world"。这意味着字符串属于可变类型。但不是这样的,我们可以检查它的身份,检查它是否是可变类型。

    1
    2
    3
    4
    5
    6
    a ="Hello"
    identity_a = id(a)
    a +=" World"
    new_identity_a = id(a)
    if identity_a != new_identity_a:
        print"String is Immutable"

    产量

    1
    String is Immutable

    证据2:

    1
    2
    a ="Hello World"
    a[0] ="M"

    产量

    1
    TypeError 'str' object does not support item assignment

    问题:元组是不变的类型吗?答:是的。证据1:

    1
    2
    3
    tuple_a = (1,)
    tuple_a[0] = (2,)
    print a

    产量

    1
    'tuple' object does not support item assignment


    在我看来,你在和一个问题斗争,那就是可变/不变实际上意味着什么。下面是一个简单的解释:

    首先,我们需要一个基础来解释。

    所以把你编程的任何东西想象成一个虚拟对象,一个保存在计算机内存中的二进制数字序列。(不过,不要太难想象。^^)现在在大多数计算机语言中,您将不会直接使用这些二进制数,而是更多地使用二进制数的解释。

    例如,您不考虑像0x110、0xAF0278297319或类似的数字,而是考虑像6这样的数字或像"hello,world"这样的字符串。这些数字或字符串绝不是计算机内存中二进制数字的一种解释。变量的任何值也是如此。

    简而言之:我们不使用实际值编程,而是使用实际二进制值的解释。

    现在我们确实有了不能为了逻辑和其他"整洁的东西"而改变的解释,而有些解释很可能会被改变。例如,考虑一个城市的模拟,换言之,一个程序中有许多虚拟对象,其中一些是房屋。现在,这些虚拟物体(房屋)可以被改变吗?它们还能被认为是相同的房屋吗?当然可以。因此,它们是可变的:它们可以在不成为"完全"不同对象的情况下进行更改。

    现在想想整数:它们也是虚拟对象(计算机内存中二进制数的序列)。所以,如果我们改变其中的一个,比如将值6增加1,它仍然是6吗?当然不是。因此,任何整数都是不可变的。

    所以:如果虚拟对象中的任何更改意味着它实际上成为另一个虚拟对象,那么它就称为不可变。

    最后备注:

    (1)不要把你的易变和不变的现实经验与用某种语言编程混淆在一起:

    每种编程语言都有自己的定义,对象可以在其中静音,而对象不能静音。

    因此,虽然您现在可以理解意义上的差异,但是您仍然需要学习每种编程语言的实际实现。…事实上,语言的目的可能是使6变为7。然后这又是一些非常疯狂或有趣的东西,比如对平行宇宙的模拟。^^

    (2)这个解释当然不科学,它是为了帮助你理解易变和不易变之间的区别。


    这个答案的目标是创建一个单一的地方来找到关于如何判断你是否正在处理变异/不变异(不可变/可变)的所有好主意,并且在可能的情况下,如何处理它?有时,突变是不可取的,在这方面,对于来自其他语言的编码人员来说,python的行为可能会感觉到违反直觉。

    根据@mina gabriel的有用帖子:

    • 阅读这本书可能会有所帮助:"Python中的数据结构和算法"
    • 摘自那本列出可变/不变类型的书:可变/可变类型图像

    分析以上内容并结合@arrak的w/a post?n:

    什么不能意外地改变?

    • scalars(存储单个值的变量类型)不会意外更改
      • 数字示例:int()、float()、complex())
    • 有一些"可变序列":
      • str(),tuple(),frozenset(),bytes()。

    什么可以?

    • 类列表对象(列表、字典、集合、bytearray())
    • 这里的帖子还提到类和类实例,但这可能取决于类继承自什么和/或如何构建。

    "出乎意料"的意思是,来自其他语言的程序员可能不会期望这种行为(有异常或Ruby,可能还有其他一些"类似于Python"的语言)。

    添加到此讨论:

    这种行为是一种优势,当它防止您意外地用占用大量数据结构的多个内存副本填充代码时。但当这是不可取的,我们如何绕过它呢?

    使用列表,简单的解决方案是创建一个这样的新列表:

    list2=列表(list1)

    其他结构…解决方案可能更棘手。一种方法是循环元素,并将它们添加到新的空数据结构(同一类型)中。

    当传入可变结构时,函数可以改变原始值。怎么说?

    • 在这个线程的其他注释上有一些测试,但是有一些注释表明这些测试不是完全证明。
    • object.function()是原始对象的一个方法,但其中只有一些会发生变化。如果他们什么也不归还,他们可能会这么做。人们会期望.append()在不测试给定名称的情况下发生变异。.union()返回set1.union(set2)的并集,并且不会发生变化。如果有疑问,可以检查函数的返回值。如果返回=无,则不会发生变化。
    • 在某些情况下,sorted()可能是解决方法。因为它返回原始版本的排序版本,所以它可以允许您在以其他方式开始处理原始版本之前存储一个未变异的副本。但是,此选项假定您不关心原始元素的顺序(如果这样做,则需要找到其他方法)。相反,.sort()会改变原始文件(正如人们所期望的那样)。

    非标准方法(如有帮助):在麻省理工学院许可证下发布的Github上找到了这个:

    • Github存储库位于:tobgu下,名为:pyrsistant
    • 它是什么:当不希望发生突变时,编写用于代替核心数据结构的python持久数据结构代码

    对于自定义类,@分号建议检查是否有__hash__函数,因为可变对象通常不应该有__hash__()函数。

    这是我目前在这个话题上积累的全部内容。欢迎其他意见、更正等。谢谢。


    A class is immutable if each object of that class has a fixed value upon instantiation that cannot SUBSEQUENTLY be changed

    换言之,更改该变量(name)的整个值,或者不使用它。

    例子:

    1
    2
    3
    my_string ="Hello world"
    my_string[0] ="h"
    print my_string

    您希望此操作正常并打印Hello World,但这将引发以下错误:

    1
    2
    3
    4
    Traceback (most recent call last):
    File"test.py", line 4, in <module>
    my_string[0] ="h"
    TypeError: 'str' object does not support item assignment

    解释器说:我不能更改这个字符串的第一个字符

    您必须更改整个string才能使其工作:

    1
    2
    3
    my_string ="Hello World"
    my_string ="hello world"
    print my_string #hello world

    检查此表:

    enter image description here

    来源


    区别的一种思考方式:

    在python中对不可变对象的赋值可以被认为是深度复制,而对可变对象的赋值是浅的


    最简单的答案是:

    可变变量是其值可能在适当位置发生变化的变量,而在不可变变量中,值的变化不会在适当位置发生。修改不可变变量将重新生成同一个变量。

    例子:

    1
    >>>x = 5

    将创建X引用的值5

    X>5

    1
    >>>y = x

    此语句将使y引用x中的5

    X---------->5<------------Y

    1
    >>>x = x + y

    因为x是一个整数(不可变类型),所以已经重新生成。

    在语句中,rhs上的表达式将生成值10,当它被分配给lhs(x)时,x将重新生成为10。所以现在

    X----> 10

    Y---->>5


    例如,对于不可变对象,赋值将创建值的新副本。

    1
    2
    3
    4
    5
    6
    x=7
    y=x
    print(x,y)
    x=10 # so for immutable objects this creates a new copy so that it doesnot
    #effect the value of y
    print(x,y)

    对于可变对象,赋值不会创建值的另一个副本。例如,

    1
    2
    3
    4
    5
    x=[1,2,3,4]
    print(x)
    y=x #for immutable objects assignment doesn't create new copy
    x[2]=5
    print(x,y) # both x&y holds the same list

    在python中,有一个简单的方法可以知道:

    不变的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
        >>> s='asd'
        >>> s is 'asd'
        True
        >>> s=None
        >>> s is None
        True
        >>> s=123
        >>> s is 123
        True

    Mutable:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    >>> s={}
    >>> s is {}
    False
    >>> {} is {}
    Flase
    >>> s=[1,2]
    >>> s is [1,2]
    False
    >>> s=(1,2)
    >>> s is (1,2)
    False

    还有:

    1
    2
    3
    >>> s=abs
    >>> s is abs
    True

    所以我认为内置函数在Python中也是不可变的。

    但我真的不明白浮动是如何工作的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    >>> s=12.3
    >>> s is 12.3
    False
    >>> 12.3 is 12.3
    True
    >>> s == 12.3
    True
    >>> id(12.3)
    140241478380112
    >>> id(s)
    140241478380256
    >>> s=12.3
    >>> id(s)
    140241478380112
    >>> id(12.3)
    140241478380256
    >>> id(12.3)
    140241478380256

    真奇怪。


    我没有阅读所有的答案,但是所选的答案是不正确的,我认为作者有一个想法,能够重新分配一个变量意味着任何数据类型都是可变的。事实并非如此。可变性与通过引用传递有关,而不是通过值传递。

    假设你创建了一个列表

    1
    a = [1,2]

    如果你说:

    1
    2
    b = a
    b[1] = 3

    即使您重新分配了B上的值,它也会重新分配A上的值。这是因为当您分配"B=A"时。您正在将"引用"传递给对象,而不是值的副本。这不是字符串、浮点数等的情况。这会使列表、字典和类似内容可变,但布尔值、浮点数等不可变。