不可变类型与可变类型

Immutable vs Mutable types

提问人:user1027217 提问时间:11/9/2011 最后编辑:Gonçalo Peresuser1027217 更新时间:9/26/2023 访问量:284524

问:

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

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

由于类结构/层次结构,这是否被认为是不可变的?,这意味着位于类的顶部,并且是它自己的方法调用。类似于这种类型的例子(即使我的书说是可变的):floatdict

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

而可变的东西在类中具有方法,例如:

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

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

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

在不调用该方法的情况下,它将返回一个字典。with 将其标记为错误。我尝试将整数传递给类,它没有标记任何错误。exampleSortedKeyDict__new__RoundFloat__new__

类型 不变性 可变

评论

0赞 agf 11/9/2011
您还可以查看使用 [:]python 何时使用 copy.copy 的列表分配,我也回答了有关可变性的更多信息。

答:

40赞 agf 11/9/2011 #1

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

ints 和 s 是不可变的。如果我这样做float

a = 1
a += 5

它将名称指向第一行内存中的某个位置。在第二行,它查找 , 添加 , 得到 ,然后在内存中指向它 -- 它没有以任何方式将 更改为 a。使用其他不可变类型的以下示例也适用相同的逻辑:a1156a616

b = 'some string'
b += 'some other string'
c = ('some', 'tuple')
c += ('some', 'other', 'tuple')

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

d = [1, 2, 3]

我创建了一个 、 和 在内存中的位置列表。如果我这样做123

e = d

我只是指向相同的列表点。然后,我可以执行以下操作:ed

e += [4, 5]

并且 和 指向的列表将更新为也具有 和 在内存中的位置。ed45

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

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

然后仍然只指向原始元组 - 您指向了一个全新的元组fg

现在,以你的例子

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

你经过的地方

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

(这是 的 ) 作为,你得到一个错误,因为 s 没有方法——你必须传递 as 才能让它工作,在这种情况下,你会得到一个空的结果。tupletuplesvaltuple.clear()dict(d)valSortedKeyDict

17赞 taleinat 11/9/2011 #2

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

用户定义的类型(即类)通常是可变的。但也有一些例外,例如不可变类型的简单子类。其他不可变类型包括一些内置类型,如 、 和 ,以及一些用 C 语言实现的 Python 类。intfloattuplestr

Python 语言参考中“数据模型”一章的一般解释:

某些对象的值可能会更改。其值可以更改的对象 据说是可变的;对象,其值一旦不可更改 被创建称为不可变。

(不可变容器的值 包含对可变对象的引用的对象可以在以下情况下更改 后者的价值发生了变化;但是,容器仍然是 被认为是不可变的,因为它包含的对象集合 无法更改。因此,不变性并不严格等同于拥有 一个不可改变的值,它更微妙。

对象的可变性是 由其类型决定;例如,数字、字符串和元组是 不可变,而字典和列表是可变的。

197赞 sebs 11/9/2011 #3

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

下面你会看到一个不可变的字符串。您无法更改其内容。如果你尝试改变它,它会引发一个。此外,如果我们分配新内容,则会创建一个新对象,而不是正在修改的内容。TypeError

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

您可以使用列表来执行此操作,它不会更改对象标识

>>> i = [1,2,3]
>>> id(i)
2146718700
>>> i[0] 
1
>>> i[0] = 7
>>> id(i)
2146718700

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

243赞 morningstar 11/9/2011 #4

什么?浮点数是不可变的吗?但是我不能

x = 5.0
x += 7.0
print x # 12.0

那不是“mut”x吗?

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

s = 'foo'
s += 'bar'
print s # foobar

变量的值会更改,但会通过更改变量引用的内容来更改。可变类型可以以这种方式更改,也可以“就地”更改。

这是区别。

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

具体例子

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]
2赞 Aditya Sihag 1/4/2013 #5

一种思考差异的方式:

python 中对不可变对象的赋值可以看作是深度拷贝, 而对可变对象的赋值是浅层的

10赞 nadapez 1/22/2013 #6

可变对象必须至少具有能够更改对象的方法。例如,该对象具有实际会改变对象的方法:listappend

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

但是该类没有改变 float 对象的方法。您可以执行以下操作:float

>>> b = 5.0 
>>> b = b + 0.1
>>> b
5.1

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

当您执行操作数时,操作数将变量绑定到新的浮点数,将创建结果为 。b = b + 0.1=5 + 0.1

将变量分配给现有对象(无论是否可变)时,操作数会将变量绑定到该对象。再也没有发生任何事情了=

无论哪种情况,只要进行绑定即可。它不会更改或创建对象。=

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

现在,如果是一个浮点数,并且你赋值,两个变量都指向同一个对象,但实际上变量之间不能通信,因为对象是不可变的,如果你这样做,现在指向一个新对象,并且仍然指向旧对象,并且不知道指向什么。ba = bb += 1bab

但是,如果 是,比方说,一个 ,你现在分配 ,并且可以“通信”,因为它是可变的,如果你这样做,那么只需检查你得到消息。clista = caclistc.append('msg')a

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

117赞 user2558887 5/18/2014 #7

常见的不可变类型:

  1. 数字:int()float()complex()
  2. 不可变序列:、、、str()tuple()frozenset()bytes()

常见的可变类型(几乎所有其他类型):

  1. 可变序列: ,list()bytearray()
  2. 套装类型:set()
  3. 映射类型:dict()
  4. 类, 类实例
  5. 等。

快速测试类型是否可变的一个技巧是使用内置函数。id()

示例,使用 on 整数、

>>> i = 1
>>> id(i)
***704
>>> i += 1
>>> i
2
>>> id(i)
***736 (different from ***704)

使用 on list,

>>> a = [1]
>>> id(a)
***416
>>> a.append(2)
>>> a
[1, 2]
>>> id(a)
***416 (same with the above id)
28赞 abarnert 4/13/2015 #8

如果你是从另一种语言(除了一种很像 Python 的语言,比如 Ruby)来学习 Python,并坚持用另一种语言来理解它,那么人们通常会感到困惑:

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

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

在 C++ 中,如果写 ,则调用 ,这将改变存储在 中的对象。(如果没有存储在 中的对象,则为错误。a = 2a.operator=(2)aa

在 Python 中,对存储在 ;它只是意味着它现在存储在 instead 中。(如果没有存储对象,那也没关系。a = 2a2aa


归根结底,这是更深层次区别的一部分。

C++ 等语言中的变量是内存中的类型化位置。如果 是 ,则表示它是编译器知道应该被解释为 的某个位置的 4 个字节。因此,当您这样做时,它会将存储在这 4 个字节的内存中的内容从 更改为 。如果其他地方有另一个 int 变量,它有自己的 4 个字节。aintinta = 20, 0, 0, 10, 0, 0, 2

Python 等语言中的变量是具有自己生命的对象的名称。数字有一个对象,数字有另一个对象。并且不是表示为 的 4 字节内存,它只是一个指向对象的名称。将数字 1 变成数字 2 是没有意义的(这会给任何 Python 程序员带来太多的权力来改变宇宙的基本运作);相反,它所做的只是让忘记对象并指向对象。12aint1a = 2a12


那么,如果赋值不是突变,那么什么是突变?

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

但是,如果赋值不是突变,那么赋值到部分对象突变又是如何呢?这就是它变得棘手的地方。 不会变异(同样,与 C++ 不同),但它确实会变异(与 C++ 不同,除非是间接的)。a[0] = ba[0]a

综上所述,最好不要尝试用你习惯的语言来表达 Python 的语义,而是按照他们自己的术语来学习 Python 的语义。

5赞 Mina Gabriel 4/6/2016 #9

如果类的每个对象在实例化时都有一个固定值,并且随后无法更改,则该类是不可变

换句话说,更改该变量的整个值或不理会它。(name)

例:

my_string = "Hello world" 
my_string[0] = "h"
print my_string 

您期望这可以工作并打印 Hello World,但这会引发以下错误:

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

my_string = "Hello World" 
my_string = "hello world"
print my_string #hello world

请看下表:

enter image description here

4赞 Markus Alpers 6/24/2016 #10

在我看来,您正在与可变/不可变的实际含义作斗争。所以这里有一个简单的解释:

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

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

例如,你不会考虑像 0x110、0xaf0278297319 或类似的数字,而是会考虑像 6 这样的数字或像“你好,世界”这样的字符串。无论如何,这些数字或字符串都是对计算机内存中二进制数的解释。对于变量的任何值也是如此。

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

现在,我们确实有一些解释,不能为了逻辑和其他“整洁的东西”而改变,而有些解释很可能会改变。例如,想想一个城市的模拟,换句话说,一个程序,其中有许多虚拟对象,其中一些是房屋。现在这些虚拟对象(房屋)可以改变吗,它们仍然可以被认为是相同的房屋吗?嗯,他们当然可以。因此,它们是可变的:它们可以被改变,而不会成为“完全”不同的对象。

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

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

结语:

(1)永远不要把你在现实世界中可变和不可变的体验与某种语言的编程混为一谈:

每种编程语言都有自己的定义,哪些对象可以静音,哪些对象不能静音。

因此,虽然您现在可能理解了含义的差异,但您仍然必须学习每种编程语言的实际实现。...事实上,一种语言可能有一个目的,即 6 可以静音变成 7。话又说回来,这将是一些疯狂或有趣的东西,比如平行宇宙的模拟^^

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

-2赞 Jason Yan 7/4/2016 #11

在 Python 中,有一种简单的方法可以知道:

变:

    >>> s='asd'
    >>> s is 'asd'
    True
    >>> s=None
    >>> s is None
    True
    >>> s=123
    >>> s is 123
    True

可变:

>>> s={}
>>> s is {}
False
>>> {} is {}
Flase
>>> s=[1,2]
>>> s is [1,2]
False
>>> s=(1,2)
>>> s is (1,2)
False

和:

>>> s=abs
>>> s is abs
True

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

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

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

这太奇怪了。

2赞 Amit Upadhyay 2/3/2017 #12

最简单的答案:

可变变量是指其值可能会就地变化的变量,而在不可变变量中,值的变化不会就地发生。修改不可变变量将重建相同的变量。

例:

>>>x = 5

将创建一个由 x 引用的值 5

x -> 5

>>>y = x

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

x -------------> 5 <-----------y

>>>x = x + y

由于 x 是整数(不可变类型)已被重建。

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

x--------->10

y--------->5

4赞 TMWP 2/14/2017 #13

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

根据 @mina-gabriel 的一篇有用的帖子:

分析上述内容并结合@arrakëën的帖子:

什么不能出乎意料地改变?

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

什么可以?

  • 列表类对象(列表、字典、集合、bytearray())
  • 这里的一篇文章也谈到了类和类实例,但这可能取决于类继承的内容和/或它的构建方式。

我所说的“出乎意料”是指来自其他语言的程序员可能不希望出现这种行为(Ruby除外,也许还有其他一些“类似Python”的语言)。

在此讨论中补充:

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

对于列表,简单的解决方案是构建一个新的列表,如下所示:

列表2 = 列表(列表1)

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

函数可以在传入可变结构时改变原始结构。怎么分辨?

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

非标准方法(如有帮助): 在 MIT 许可下发布的 github 上找到了这个:

  • GitHub 存储库位于:Tobgu 下,命名为:pyrsistent
  • 它是什么:Python 持久性数据结构代码,用于在不需要突变时代替核心数据结构

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

这就是我目前在这个主题上积累的全部内容。欢迎其他想法、更正等。谢谢。

-1赞 Scott 7/5/2017 #14

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

假设您创建了一个列表

a = [1,2]

如果你要说:

b = a
b[1] = 3

即使您在 B 上重新分配了值,它也会在 a 上重新分配该值。这是因为当您分配“b = a”时。您正在将“引用”传递给对象,而不是值的副本。字符串、浮点数等并非如此。这使得列表、字典等是可变的,但布尔值、浮点数等是不可变的。

30赞 Anand Tripathi 11/13/2017 #15

可变对象和不可变对象之间的区别

定义

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

在 Python 中,如果更改不可变对象的值,它将创建一个新对象。

可变对象

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

  1. list
  2. Dictionary
  3. Set
  4. bytearray
  5. user defined classes

不可变对象

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

  1. int
  2. float
  3. decimal
  4. complex
  5. bool
  6. string
  7. tuple
  8. range
  9. frozenset
  10. bytes

一些悬而未决的问题

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

a = "Hello"
a +=" World"
print a

输出

"Hello World"

在上面的示例中,字符串曾经创建为“Hello”,然后更改为“Hello World”。这意味着字符串是可变类型的。但是,当我们检查它的身份以查看它是否属于可变类型时,情况并非如此。

a = "Hello"
identity_a = id(a)
a += " World"
new_identity_a = id(a)
if identity_a != new_identity_a:
    print "String is Immutable"

输出

String is Immutable

证据 2

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

输出

TypeError 'str' object does not support item assignment

元组是不可变的类型吗?
是的证据 1

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

输出

'tuple' object does not support item assignment
-2赞 Ravi Gurnatham 12/31/2018 #16

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

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)

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

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
1赞 Gonçalo Peres 1/21/2021 #17

可变意味着它可以改变/变异。不可变,反之亦然。

某些 Python 数据类型是可变的,而另一些则不可变。

让我们找出适合每个类别的类型,并查看一些示例。


可变

在 Python 中,有各种可变类型:

  • 列表

  • 字典

  • 设置

让我们看下面的示例。lists

list = [1, 2, 3, 4, 5]

如果我执行以下操作来更改第一个元素

list[0] = '!'
#['!', '2', '3', '4', '5']

它工作得很好,因为列表是可变的。

如果我们考虑该列表,则该列表已更改,并为其分配一个变量

y = list

如果我们从列表中更改一个元素,例如

list[0] = 'Hello'
#['Hello', '2', '3', '4', '5']

如果有人打印它会给y

['Hello', '2', '3', '4', '5']

由于两者都指的是同一个列表,因此我们更改了列表。listy


在某些编程语言中,可以定义一个常量,如下所示

const a = 10

如果有人打电话,它会给出错误

a = 20

但是,这在 Python 中不存在。

但是,在 Python 中,有各种不可变类型:

  • 没有

  • 布尔

  • 整数

  • str

让我们看下面的示例。strings

拿绳子a

a = 'abcd'

我们可以得到第一个元素

a[0]
#'a'

如果尝试为第一个位置的元素分配一个新值

a[0] = '!'

它会给出一个错误

“str”对象不支持项赋值

当一个人对字符串说 += 时,例如

a += 'e'
#'abcde'

它不会给出错误,因为它指向不同的字符串。a

它与以下内容相同

a = a + 'f'

并且不更改字符串。

不可变的一些优点和缺点

• 内存中的空间从一开始就是已知的。它不需要额外的空间。

• 通常,它使事情更有效率。例如,查找字符串的速度要快得多,因为它是字符串对象的一部分。len()

2赞 Shaiful Islam 10/12/2022 #18

每次我们更改不可变变量的值时,它基本上都会销毁以前的实例并创建一个变量类的新实例

var = 2 #Immutable data
print(id(var))
var += 4
print(id(var))

list_a = [1,2,3] #Mutable data
print(id(list_a))
list_a[0]= 4
print(id(list_a))

输出:

9789024
9789088
140010877705856
140010877705856

注意:可变变量memory_location当我们更改值时会发生变化

-1赞 Gabriel Staples 9/26/2023 #19

更新:忽略我在这个答案中的错误观点。所有变量都通过引用传递。我稍后会修复这个答案。请参阅此评论,以及我的答案下方的评论: 不可变类型与可变类型

我选择在修复它时不删除它,以便我可以先在评论中获得更多反馈和更正。


Python 中的可变类型与不可变类型

1. 总结

  1. 在简单的英语中,可变的意思是可变的。想想:“突变”。

  2. 不可变意味着不可更改

  3. Python 没有常量的概念。不可变可变并不意味着常不常量。相反,它分别意味着不可变的 --> 共享内存(通过动态内存分配在内存中的单个底层对象,对于分配给变量的给定文本值)与可变 -->非共享内存(通过动态内存分配在内存中分配多个底层对象,对于分配给变量的给定文本值)。我在下面有更多关于这方面的内容,因为这是非常微妙的。

    1. 因此,这也意味着通过引用传递(可变)与通过值传递(不可变),因为能够在内存中维护自己唯一底层对象的对象可以通过引用传递这些可变内存块,以便它们可以被变异。
  4. Python 中的一切都是一个对象。偶数、整数、浮点数等都是对象。所有变量都是对象。

  5. 下面列出了 Python 中的可变对象与不可变对象类型。

  6. 可变类型通过引用传递,并导致副作用

    1. 如果你这样做,那么改变,它也会改变。这是一个副作用。这是因为每个变量都指向相同的基础对象(内存 blob)。my_dict3 = my_dict2 = my_dict1 = {}my_dict3my_dict2my_dict1

    2. 同样,如果以链式方式分配可变类型,则每个可变变量都指向内存中的同一底层对象,因为该值通过引用从一个变量传递到下一个变量:

      my_dict1 = {"key": "value"}
      # Copy **by reference**, so all variables point to the same 
      # underlying object.
      my_dict2 = my_dict1
      my_dict3 = my_dict2
      

      因此,以下都是True

      # Each of these is True because the underlying object is the same
      # blob of memory.
      print(my_dict3 is my_dict2)    # True
      print(my_dict2 is my_dict1)    # True
      print(my_dict3 is my_dict1)    # True
      # And each of these is True because all variables have the same value.
      print(my_dict3 == my_dict2)    # True
      print(my_dict2 == my_dict1)    # True
      print(my_dict3 == my_dict1)    # True
      
    3. 但是,如果独立地将相同的文本值分配给可变类型,则每个可变变量都指向内存中自己的基础对象,因为它应该能够独立地改变它所指向的内存:

      # **Mutable type:** each variable has an **independent underlying 
      # object**, even though each of those underlying objects has the same
      # value.
      my_dict1 = {"key": "value"}
      my_dict2 = {"key": "value"}
      my_dict3 = {"key": "value"}
      # Therefore, each of these is False because the underlying objects 
      # differ.
      print(my_dict3 is my_dict2)    # False
      print(my_dict2 is my_dict1)    # False
      print(my_dict3 is my_dict1)    # False
      # But, each of these is True because all variables have the same value.
      print(my_dict3 == my_dict2)    # True
      print(my_dict2 == my_dict1)    # True
      print(my_dict3 == my_dict1)    # True
      
    4. 如果将可变变量传递给函数,并且该函数对其进行了修改,则修改将自动在函数外部看到。这是一个副作用

      def modify_dict(my_dict):
          my_dict["new_key"] = "new_value"
      
      my_dict1 = {"key": "value"}
      modify_dict(my_dict1)
      print(my_dict1)  # prints: {"key": "value", "new_key": "new_value"}
      
    5. 若要强制按值而不是按引用传递可变类型,可以调用该方法来强制创建基础对象的副本。.copy()

      my_dict1 = {"key": "value"}
      # Force-copy **by value**, so each variable has its own underlying 
      # object. The `.copy()` method makes an entirely new copy of the 
      # underlying object.
      my_dict2 = my_dict1.copy()
      my_dict3 = my_dict2.copy()
      # Therefore, each of these is False because the underlying objects 
      # differ.
      print(my_dict3 is my_dict2)    # False
      print(my_dict2 is my_dict1)    # False
      print(my_dict3 is my_dict1)    # False
      # But, each of these is True because all variables have the same value.
      print(my_dict3 == my_dict2)    # True
      print(my_dict2 == my_dict1)    # True
      print(my_dict3 == my_dict1)    # True
      
  7. 不可变类型通过复制传递,会引起副作用

    1. 如果你这样做了,那么改变,它不会改变或,因为那将是一个副作用。它没有副作用。my_int3 = my_int2 = my_int1 = 1my_int3my_int2my_int1

    2. 但是,如果将相同的值分配给多个不可变变量,无论是通过链式赋值还是独立文本赋值,变量都相等 ( is ) 并且相同 ( is ),如下所示:var1 == var2Truevar1 is var2True

      ## **Immutable types:** each variable apparently has the **same 
      # underlying object**, but side effects are not allowed
      my_int1 = 7
      my_int2 = 7
      my_int3 = 7
      # Therefore, each of these is True because the underlying objects are 
      # the same.
      print(my_int3 is my_int2)      # True
      print(my_int2 is my_int1)      # True
      print(my_int3 is my_int1)      # True
      # And, each of these is also True because all variables have the 
      # same value.
      print(my_int3 == my_int2)      # True
      print(my_int2 == my_int1)      # True
      print(my_int3 == my_int1)      # True
      
      # Try the test again, this time like this
      my_int1 = 7
      my_int2 = my_int1
      my_int3 = my_int2
      # Same as above: same underlying object, so each of these is True
      print(my_int3 is my_int2)      # True
      print(my_int2 is my_int1)      # True
      print(my_int3 is my_int1)      # True
      # Same as above: same value, so each of these is True
      print(my_int3 == my_int2)      # True
      print(my_int2 == my_int1)      # True
      print(my_int3 == my_int1)      # True
      print()
      

      这有点令人困惑,但不变性和缺乏副作用是正确的。

    3. 如果将不可变变量传递给函数,并且该函数对其进行了修改,则在函数外部将看不到修改。没有副作用。相反,如果要在函数外部查看更改,则必须从函数返回修改后的变量,并将其重新赋值到函数外部。

      def modify_int(my_int):
          my_int += 1
          return my_int
      
      my_int1 = 7
      # reassign the returned value to obtain the change from inside the
      # function
      my_int1 = modify_int(my_int1)  
      print(my_int1)  # prints: 8
      
    4. 若要强制通过引用而不是复制传递不可变类型,只需将不可变类型包装在可变类型(如列表)中,然后将可变包装器传递给函数即可。这样,它就会通过引用传递,并且修改其内容的副作用会自动在函数外部可见:

      def modify_immutable_type_hack(var_list):
          var_list[0] += 1  # increment the number inside the first element
      
      my_int = 10
      my_int_list = [my_int]
      print(my_int_list[0])  # 10
      # Force an immutable type to act mutable by passing it inside a list,
      # which is a mutable type, into a function. This way, the "side effect"
      # of the change to the list being visible outside the function is still
      # seen. This is because the list gets passed **by reference** instead
      # of **by value.**
      modify_immutable_type_hack(my_int_list)
      print(my_int_list[0])  # 11
      print(my_int)          # 10
      

      但是,对于单个数字,更清楚的是只使用上面的返回值方法:.my_int1 = modify_int(my_int1)

  8. 您可以编写一个程序,通过查找和识别副作用来自动识别类型是可变的还是不可变的。我在下面这样做。

  9. Python 是一种简单的编程语言。它有很多这样的细微差别。它只是流行,不同,而且水平很高。

上述学习的所有测试代码如下。

2. Python 中可变对象与不可变对象列表

以下是 Python 中最常见的可变和不可变对象列表。这个列表可以通过以下方式获得:1)在Python的官方“内置类型”参考页面中搜索“可变”和“不可变”这两个词,或者2)向GitHub Copilot(或BingAI或ChatGPT)询问。我两者都做了。当然,后者要快得多,但需要验证。我根据自己的发现验证并更新了下面的列表,并添加了所有引文,主要来自官方文档。

可变对象:

  • list - “列表是可变序列”
  • set - “集合类型是可变的”
  • dict - “映射对象将可哈希值映射到任意对象。映射是可变对象。目前只有一种标准的映射类型,即字典
  • 字节数组
    • “由于字节数组对象是可变的,因此除了公共字节和字节数组操作外,它们还支持可变序列操作”
    • “ByteArray 对象是可变的,并且具有高效的过度分配机制”
    • “ByteArray 对象是 bytes 对象的可变对应物。”

不可变对象:

  • 所有数值类型。官方的 Python 文档没有提到它们的不可变性,但我找到的每个来源和 AI 都确认它们是不可变的,就像我下面的个人测试一样。来自 RealPython.com:“数字类型......是不可变的。
    • 整数
    • 复杂
    • 布尔
  • str - “字符串是 Unicode 码位的不可变序列。”
  • bytes - “Bytes 对象是单个字节的不可变序列。”
  • tuple - “元组是不可变的序列,通常用于存储异构数据的集合”
  • frozenset - “frozenset 类型是不可变且可散列的”
  • range - “范围类型表示不可变的数字序列,通常用于在 for 循环中循环特定次数。”

如果你愿意,就停在这里。以上是最重要的。


3. 重新分配

所有变量都可以在 Python 中重新分配,无论它们以前是分配给可变类型还是不可变类型。但是,对于可变类型和不可变类型,重新分配的行为是不同的,不能纯粹在传统的 C 和 C++ 类内存术语和理解中考虑。Python 就是 Python,而 Python 是不同的。

来自C和C++作为我的主要语言,Python中的“可变性”概念非常令人困惑。出于这个原因和其他原因,我不认为 Python 是一种“简单”或“初学者”语言。它只是一种非常强大的“超高级”语言,仅此而已。它有很多非常微妙和令人困惑的点。在这方面,C更直接、更具体。(C++简直是疯了)。

在 C 和 C++ 中,我对每个变量的心智模型是它是内存中的字节块。执行此操作时:

int var = 0;  // statically allocate bytes for `var`, and mutate them into a 0
var = 7;      // mutate the bits in `var` into a 7 now instead of a 0

...您正在将 的内存块中的字节从存储 A 的位更改为存储 A 的位。您已经“变异”了该变量分配的内存,这意味着:为它设置的内存块。变量的类型只是指定“透镜”(想想:通过魔术玻璃透镜观察,将位解释为数字、字母等)或解释算法,通过它你将解释这些位(例如:、、等)。var07floatintchar

然而,在 Python 中,这不是你应该为变量建立的心智模型。在 Python 中,将变量视为包含指向其他对象的指针的对象其中所有内容都是一个对象,每个对象都包含一个位,指定它是可变的还是不可变的,可变变量通过引用传递,而不可变变量通过值传递。另请注意,对象是一个非常复杂的动态分配的类实例,它管理自己的状态和内存块,有点像 C++ 中的 std 容器

通过执行以下操作,您可以立即看到类型、、等都是 Python 中的(对象),而不仅仅是像 C 和 C++ 中的普通类型:intboolfloat

type(7)     # returns <class 'int'>
type(True)  # returns <class 'bool'>
type(1.0)   # returns <class 'float'>

因此,在 Python 中,当您执行此操作时:

# dynamically allocate a pointer variable object named `var`, then
# dynamically allocate an integer object with a `0` in it, then point `var`
# to that int object which contains a `0`.
var = 0  # 0 is the value contained inside an immutable type `int` object

# dynamically allocate a new integer object with a `7` in it, then point `var`
# to that new, underlying int object which contains a `7`. Therefore,
# this simply re-assigns the variable `var` from pointing to the `0`
# object, to  pointing to the `7` object; the `int` object with a `0`
# in it is now "orphaned" and I suspect will be garbage collected, but that 
# level of understanding is beyond me.
var = 7

对于可变对象,重新赋值会就地修改其字节,而不是动态创建新对象并将变量指向该对象。例如:

my_dict = {}  # dynamically allocate a pointer variable named `my_dict`,
              # dynamically allocate a dict object, then point `my_dict` to
              # that dict object
my_dict["some_key"] = "some_value"  # mutate the dict object by adding a
                                    # key-value pair

对于Python程序员来说,“不可变”与“可变”特性的细节并不重要,就像对像我这样的低级嵌入式系统C和C++程序员来说。相反,它有点“挥手”。Python 程序员应该只是盲目地接受它并继续前进。一些高级C++程序员也是这样。这是一个心态问题。

所以,只要你知道上面第 1 节中的摘要,以及上面第 2 节中 Python 中可变对象与不可变对象的列表,你就很好了。

4. 引用传递与复制传递,以及副作用

然而,一个好的 Python 程序员需要知道的是,当传递给函数时,他们的特定变量类型是通过引用还是通过复制传递。这是一个非常重要的区别,因为通过引用传递会产生副作用,这意味着在一个地方对变量的更改将导致对该变量或其他地方其他变量的更改,例如函数内部与外部。这是 Python 中一个非常重要的概念,对我来说,这也是区分可变类型和不可变类型很重要的主要原因。我真的不在乎它们在引擎盖下是如何工作的。

再:

  1. 可变类型传递给函数会导致副作用,这意味着对函数内部变量的修改将在函数外部看到。
  2. 不可变类型传递给函数不会引起副作用,这意味着在函数外部看不到对函数内部变量的修改。

5. 与is==

运算符测试变量中存储的值是否相等。我认为运算符用于测试两个变量是否引用内存中的同一对象。同样,Python 如何在引擎盖下做到这一点的细微差别超出了我的范围。但是,请参阅我的第 1 节摘要,了解 vs 的一系列细微差别情况。==isis==

官方 Python 文档在其文档的“标识比较”部分下描述了 and 运算符,如下所示: https://docs.python.org/3/reference/expressions.html#is-notisis not

对象标识的运算符 和 test 为 true,当且仅当 和 是同一对象。对象的身份是使用该函数确定的。 产生反真值。isis notx is yxyid()x is not y

该函数说: https://docs.python.org/3/library/functions.html#idid()

返回对象的“标识”。这是一个整数,保证该对象在其生存期内是唯一且常数的。具有非重叠生存期的两个对象可能具有相同的值。id()

因此,我将其解释为是一个唯一的动态内存地址,或索引到一个映射,在创建对象时分配给每个对象。当您这样做时,所有这 4 个部分都是相同的:id()my_int3 = my_int2 = my_int1 = 7id()

my_int3 = my_int2 = my_int1 = 7
print(id(my_int1))  # 140201501393328
print(id(my_int2))  # 140201501393328
print(id(my_int3))  # 140201501393328
print(id(7))        # 140201501393328

因此,它们似乎都是相同的底层对象或内存斑点。

6. 一些测试代码

这是我的测试代码。我的测试代码中几乎所有的行为对我来说都是不可预测的,因为 Python 是一种与 C 和 C++ 截然不同的语言,在运行它们和进行此学习之前,我无法猜测大多数测试的结果。我几乎什么都错了。

mutable_vs_immutable_types.py来自我的eRCaGuy_hello_world存储库。

注意:测试代码太长,无法粘贴到此答案中,因为堆栈溢出限制为 30000 个字符。因此,请在上面的链接中查看。

示例运行和输出:

eRCaGuy_hello_world$ python/mutable_vs_immutable_types.py 
var before modification: True
var is a bool
var after modification:  False

var before modification: 1.0
var is a float
var after modification:  2.0

var before modification: 7
var is an int
var after modification:  8

var before modification: some words
var is a str (string)
var after modification:  some words; some more words

var before modification: [7, 8, 9]
var is a list
var after modification:  [7, 8, 9, 1]

var before modification: {'key1': 'value1', 'key2': 'value2'}
var is a dict
var after modification:  {'key1': 'value1', 'key2': 'value2', 'new_key': 'new_value'}

var before modification: (7, 8, 9)
var is a tuple
var after modification:  (1, 2, 3)

is_mutable(my_bool, True)                                    -->  immutable
is_mutable(my_float, 1.0)                                    -->  immutable
is_mutable(my_int, 7)                                        -->  immutable
is_mutable(my_str, "some words")                             -->  immutable
is_mutable(my_list, [7, 8, 9])                               -->  mutable
is_mutable(my_dict, {"key1": "value1", "key2": "value2"})    -->  mutable
is_mutable(my_tuple, (7, 8, 9))                              -->  immutable

int is immutable
list is mutable

MUTABLE TYPES
False
False
False
True
True
True

IMMUTABLE TYPES
True
True
True
True
True
True

integer types again
True
True
True
True
True
True

True
True
True
True
True
True

False
False
False
True
True
True

How to update immutable vs mutable variables in a function:

7
8

[7, 8, 9]
[7, 8, 9, 1]

10
11
10

引用

  1. 几乎所有的参考资料都是我上面的链接和测试代码。
  2. 这是我自己的工作,但对于任何好奇的人,以下是我与 GitHub Copilot 的介绍性对话,以让我入门: