不可变类型与可变类型

2024-11-22 08:47:00
admin
原创
6
摘要:问题描述:我对不可变类型感到困惑。我知道对象float被认为是不可变的,我的书中有这种类型的例子:class RoundFloat(float): def __new__(cls, val): return float.__new__(cls, round(val, 2)) 这是否因为...

问题描述:

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

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

这是否因为类结构/层次结构而被视为不可变的?这意味着float它位于类的顶部并且是它自己的方法调用。类似于这种类型的例子(尽管我的书上说它dict是可变的):

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

如果不调用该example方法,它将返回一个字典。with 将其标记为错误。我尝试将整数传递给SortedKeyDictwith类,它没有标记任何错误。__new__`RoundFloat`__new__


解决方案 1:

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

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:

您必须了解 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 语言参考:

  • Python 2 数据模型

  • Python 3 数据模型

解决方案 3:

常见的不可变类型:

  1. 数字:int(),,float()`complex()`

  2. 不可变序列:str(),,,tuple()`frozenset()`bytes()

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

  1. 可变序列:list()bytearray()

  2. 设置类型:set()

  3. 映射类型:dict()

  4. 类、类实例

  5. ETC。

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

例如,使用整数,

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

使用列表,

>>> a = [1]
>>> id(a)
***416
>>> a.append(2)
>>> a
[1, 2]
>>> id(a)
***416 (same with the above id)

解决方案 4:

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

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

a = 1
a += 5

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

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

对于可变类型,我可以执行实际更改其存储在内存中的值的操作。使用:

d = [1, 2, 3]

1我在内存中创建了、2和的位置列表3。如果我这样做

e = d

我只需指向e相同list d点即可。然后我可以这样做:

e += [4, 5]

e并且和指向的列表也将被更新,以在内存中包含和的d位置。4`5`

如果我回到不可变类型并使用以下方式执行此操作tuple

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

然后f仍然 只 指向原来 的tuple-- 你 已经 指向g一个全新 的tuple.

现在,以你的例子来说

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

你经过的地方

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

(它是tupletuples)因为val,您会收到一个错误,因为tuples 没有.clear()方法——您必须传递dict(d)asval才能使其工作,在这种情况下,您会得到一个空的SortedKeyDict结果。

解决方案 5:

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

定义

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

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

在 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”,后来被改为“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"

输出

字符串是不可变的

证明2

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

输出

TypeError“str”对象不支持项目分配

问题Tuple 是不可变类型吗?

答案:是
证明 1

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

输出

“tuple”对象不支持项目分配

解决方案 6:

如果你从另一种语言(除了与 Python 非常相似的语言,如 Ruby)转到 Python,并坚持从另一种语言的角度来理解它,那么人们通常会感到困惑:

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

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

在 C++ 中,如果你写了a = 2,那么你正在调用a.operator=(2),这将改变 中存储的对象a。 (如果没有存储在 中的对象a,那就是一个错误。)

在 Python 中,a = 2对 中存储的内容不执行任何操作a;它只是意味着2现在 存储在 中a。(如果没有对象存储在 中a,那也没关系。)


从根本上来说,这是更深层次区别的一部分。

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

在 Python 等语言中,变量是具有自身生命周期的对象的名称。数字 有一个对象1,数字 有另一个对象2a表示为 的 不是 4 个字节的内存int,它只是指向对象的名称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不会变异a[0](再次强调,与 C++ 不同),但它变异a(与 C++ 不同,除了间接变异)。

所有这些都是为什么最好不要尝试用你习惯的语言来理解 Python 的语义,而是用他们自己的术语来学习 Python 的语义。

解决方案 7:

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

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

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

某些对象的值可以改变。值可以改变的对象称为可变的;一旦创建,值就不可改变的对象称为不可变的。

(包含对可变对象的引用的不可变容器对象的值可以在后者的值改变时改变;但是容器仍然被认为是不可变的,因为它包含的对象集合不能改变。所以,不变性并不严格等同于具有不可改变的值,它更微妙。)

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

解决方案 8:

可变对象必须至少具有一种能够改变对象的方法。例如,对象list具有以下append方法,该方法实际上会改变对象:

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

但是该类float没有方法来改变浮点对象。你可以这样做:

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

=操作数不是方法。它只是在变量和它右边的任何内容之间建立绑定,没有其他内容。它永远不会改变或创建对象。从现在起,它是变量将指向什么的声明。

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

当你将变量赋值给一个现有的对象时,无论是否可变,=操作数都会将变量绑定到该对象。除此之外什么也不会发生

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

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

现在,如果b是一个浮点数并且你分配了a = b,两个变量都指向同一个对象,但实际上变量不能在它们之间进行通信,因为对象是不可变的,如果你这样做b += 1,现在b指向一个新对象,并且a仍然指向旧对象并且不知道b指向什么。

但是如果c是,假设是,list并且您分配a = c,现在ac可以“通信”,因为list是可变的,并且如果您这样做c.append('msg'),那么只需检查a您是否收到了消息。

(顺便说一下,每个对象都有一个与之关联的唯一 ID 号,您可以通过获取该 ID 号来获取id(x)。因此,您可以通过检查对象是否相同或不检查其唯一 ID 是否已更改。)

解决方案 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

检查下表:

在此处输入图片描述

来源

解决方案 10:

在我看来,你正在纠结可变/不可变到底意味着什么。因此,这里有一个简单的解释:

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

因此,请将您编写的任何程序视为虚拟对象,即以二进制数字序列形式保存在计算机内存中的东西。(不过,不要对此进行过多的想象。^^)现在,在大多数计算机语言中,您不会直接使用这些二进制数字,而是更多地使用二进制数字的解释。

例如,您不会考虑 0x110、0xaf0278297319 或类似的数字,而是考虑 6 之类的数字或“Hello, world”之类的字符串。尽管如此,这些数字或字符串仍然是计算机内存中二进制数字的解释。变量的任何值也是如此。

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

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

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

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

最后的评论:

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

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

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

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

解决方案 11:

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

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

  • 可能有帮助的书籍:“ Python 中的数据结构和算法”

  • 列出可变/不可变类型的书中摘录:
    可变/不可变类型图像

分析以上内容并结合@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() 会变异原始数据(正如人们所预料的那样)。

非标准方法(如果有用):在 github 上找到了根据 MIT 许可发布的:

  • github 存储库位于:tobgu,名称为:pyrsistent

  • 它是什么:Python 持久数据结构代码,用于在不希望发生突变时代替核心数据结构

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

这是我目前关于这个主题的所有想法。欢迎提出其他想法、更正等。谢谢。

解决方案 12:

思考差异的一种方式:

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

解决方案 13:

最简单的答案:

可变变量的值可以就地改变,而不可变变量的值不会就地改变。修改不可变变量将重建相同的变量。

例子:

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

解决方案 14:

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

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的值时,该变量也会改变

解决方案 15:

可变意味着它可以改变/变异。不可变则相反。

一些 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 中,有各种不可变类型:

  • 没有任何

  • 布尔值

  • 整数

  • 漂浮

  • 字符串

  • 元组

让我们看看下面的例子strings

拿起琴弦a

a = 'abcd'

我们可以使用

a[0]
#'a'

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

a[0] = '!'

它会给出一个错误

‘str’ 对象不支持项目分配

当对字符串使用 += 时,例如

a += 'e'
#'abcde'

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

这与以下内容相同

a = a + 'f'

并且不改变字符串。

不可变性的优点和缺点

• 从一开始就知道内存空间。不需要额外的空间。

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

解决方案 16:

在 Python 中,每种数据类型都是一个类(一切皆对象)

var1 = 5 

var1是变量,指向对象的名称标签(保存对象地址,就像 c 中的指针)

id(var1) #returns the memory address storing int type object with value 5

5是存储在内存中的 int 类的对象实例

如果我对 var1 执行一些操作

var1 += 3

int、float、string、tuple 等都是不可变的。它们在创建后不能改变值。这不会改变 var1 当前指向的内存中存储的对象 5,而是取该对象的值 5,加上 3 得到 8,然后创建一个新的 int 对象,将值 8 存储在不同的位置,并将 var1 重定向到新对象的内存位置

var1 = 5
print(id(var1))

var1 += 3
print(id(var1))

这将打印两个不同的内存地址,分别存储 int obj 5 和 8

对于可变类型列表、集合、字典……

var1 = [1, 2, 3]
print(id(var1))

var1 += [4,5]
print(id(var1))

这将打印相同的内存地址,显示var1仍然指向相同的内存地址,[1, 2, 3]在某一时间点保存一个列表类型的对象,然后[1, 2, 3, 4, 5]在最后一次打印时保存添加元素的相同对象

这样,它不会创建新的实例,但相同的 obj 实例会发生变异以包含添加的元素。

不可变对象内存管理

由于此对象在创建后无法更改值,因此更改任何内容都意味着创建一个新对象。python 通过 Interning 避免创建重复的对象。它有一个对象列表、对象类型及其正在使用或在内存中的值,如果将新变量定义为某个值的新不可变对象,它将检查其在内存中的对象列表,如果内存中具有具有相同值的此类型的对象,它只需将分配的新变量指向内存中具有相同值的对象,而不是创建具有相同值的新变量。多个变量重用内存中的同一个对象,而不会对引用它们的变量产生任何副作用,因为对象值无法改变。

var1 = 1006
print(id(var1))

var2 = 1006
print(id(var2))

它将打印相同的地址,因为两者都指向同一个对象,即使你单独定义变量,而不是通过别名var1 = var2 = 1006并且它是不可变的类型

它跟踪该对象和指向它的变量,如果没有变量再引用它,它将被传递给垃圾收集器进行删除

事实上,在 Python CPython 的默认实现中,小整数(通常在 -5 到 256 范围内)会被缓存并重用。这种优化称为“整数驻留”。当 Python 启动时,它会为小值(通常在 -5 到 256 之间)创建一个整数对象池。内存池中每个整数只有一个实例。当您创建一个变量并为其分配一个小整数值时,Python 会检查该值是否在驻留范围内。如果在,它会重用现有对象而不是创建新对象。

此行为是一种优化策略,旨在减少内存消耗并提高常用小整数的性能。

解决方案 17:

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

假设你创建了一个列表

a = [1,2]

如果你要说:

b = a
b[1] = 3

即使你重新分配了 B 的值,它也会重新分配 a 的值。这是因为当你分配“b = a”时。你传递的是“引用”给对象,而不是值的副本。字符串、浮点数等不是这种情况。这使得列表、字典等可变,但布尔值、浮点数等不可变。

解决方案 18:

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

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


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

  1. 总结


  1. 简单来说,Mutable 的意思是可变的。想想“突变”。

  2. 不可变的意思是不可改变的

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

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

  2. Python 中可变与不可变对象类型如下。

  3. 可变类型通过引用传递,并会引起副作用。**

1. 如果你执行`my_dict3 = my_dict2 = my_dict1 = {}`,然后更改`my_dict3`,它*也会*更改`my_dict2` *和* `my_dict1`。这是一个*副作用。*这是因为每个变量都指向*相同的*底层对象(内存 blob)。
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
  1. 不可变类型通过复制传递,不会产生副作用。**

1. 如果你执行`my_int3 = my_int2 = my_int1 = 1`,然后更改`my_int3`,它*不会*更改`my_int2`或`my_int1`,因为那将是一个副作用。它没有*副作用。*
2. 但是,如果将相同的值分配给多个不可变变量,无论是通过*链式赋值* *还是* *独立文字赋值*,这些变量都是相等的(`var1 == var2`is `True`)*并且*它们是相同的(`var1 is var2`is `True`),如下所示:


## **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)`。
  1. 你可以编写一个程序,通过查找和识别副作用来自动识别类型是可变的还是不可变的。我在下面这样做。

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

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

  1. Python 中可变对象与不可变对象的列表


以下是 Python 中最常见的可变和不可变对象的列表。可以通过以下方式获取此列表:1) 仔细搜索Python 官方“内置类型”参考页面中的“可变”和“不可变”字样,2) 向 GitHub Copilot(或 BingAI 或 ChatGPT)询问。我两种方法都试过了。当然,后者速度更快,但需要验证。我根据自己的发现验证并更新了下面的列表,并添加了所有引文,大部分来自官方文档。

可变对象:

  • 列表——“列表是可变的序列”

  • 设置-“集合类型是可变的”

  • 字典- “映射对象将可哈希值映射到任意对象。映射是可变对象。目前只有一种标准映射类型,即字典。”

  • 字节数组

    • “由于字节数组对象是可变的,因此除了常见的字节和字节数组操作之外,它们还支持可变序列操作”

    • “bytearray 对象是可变的,并且具有有效的过度分配机制”

    • “bytearray 对象是 bytes 对象的可变对应物。”

不可变对象:

  • 所有数字类型。官方 Python 文档没有提及它们的不可变性,但我发现的每个来源和 AI 都证实它们是不可变的,下面是我的个人测试。来自RealPython.com: “数字类型...是不可变的。”

    • 整数

    • 漂浮

    • 复杂的

    • 布尔值

  • str - “字符串是 Unicode 代码点的不可变序列。”

  • 字节- “字节对象是单个字节的不可变序列。”

  • tuple - “元组是不可变的序列,通常用于存储异构数据的集合”

  • frostyset — “frozenset 类型是不可变且可哈希化的”

  • 范围- “范围类型表示不可变的数字序列,通常用于在 for 循环中循环特定次数。”


如果你愿意的话就到此为止吧。以上是最重要的。


  1. 重新分配


在 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

var...您正在将 的内存块中的字节从存储 的位更改0为存储 的位7。您已经“变异”了该变量分配的内存,即:为其预留的内存块。变量的类型只是指定了“镜头”(想象一下:透过一个将位解释为数字、字母等的魔镜镜头)或解释算法,您将通过该算法解释这些位(例如:floatintchar等)。

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

通过执行以下操作,您可以立即看到类型int、、等都是Python 中的(对象),而不仅仅是 C 和 C++ 中的简单类型:bool`float`

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. 将不可变类型传递给函数不会引起副作用,这意味着对函数内部该变量的修改不会函数外部看到。

  3. isvs==


运算符==测试变量中存储的值是否相等。我认为,运算符用于测试两个变量是否引用内存中的同一个对象。同样,Python 内部如何实现这一点的细微差别超出了我的理解范围。不过,请参阅我的第 1 节摘要,其中列出了vsis的一系列细微差别。is`==`

官方 Python 文档在其文档的“身份比较”部分下描述了isand运算符:https: //docs.python.org/3/reference/expressions.html#is-not:is not

运算符isis not测试对象的身份:x is y当且仅当xy是同一个对象时才为真。对象的身份使用函数确定id()x is not y产生逆真值。

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

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

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

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

注意:测试代码太长,无法粘贴在此答案中,因为 Stack Overflow 限制为 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 进行的介绍性对话,以帮助我入门:

解决方案 19:

在 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

这太奇怪了。

解决方案 20:

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

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
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   609  
  在现代项目管理中,资源的有效利用是确保项目成功的关键因素之一。随着技术的不断进步,越来越多的工具和软件被开发出来,以帮助项目经理和团队更高效地管理资源。本文将介绍10款工具,这些工具可以帮助项目团队提升资源利用效率,从而实现项目目标。禅道项目管理软件禅道项目管理软件是一款开源的项目管理工具,广泛应用于软件开发和其他行业...
项目管理系统   3  
  在项目管理领域,软件工具的不断升级和创新是推动效率和协作的关键。2024年,众多项目管理软件将迎来一系列令人期待的升级功能,这些新特性不仅将提升团队的工作效率,还将增强用户体验和数据分析能力。本文将详细介绍10款项目管理软件的最新升级功能,帮助项目经理和团队成员更好地规划和执行项目。禅道项目管理软件禅道项目管理软件一直...
开源项目管理工具   2  
  信创国产系统的10个关键厂商及其技术生态随着全球信息技术格局的不断演变,信创(信息技术应用创新)产业作为国产化替代的重要阶段,正逐步成为推动我国信息技术自主可控、安全可靠的核心力量。信创产业不仅关乎国家信息安全,也是数字经济高质量发展的关键支撑。本文将深入探讨信创国产系统中的10个关键厂商及其技术生态,分析它们在信创浪...
项目管理流程   0  
  在探讨项目管理的广阔领域中,成功并非偶然,而是精心策划、高效执行与持续优化的结果。项目管理的成功之道,可以从明确的目标设定与规划、高效的团队协作与沟通、以及灵活的风险管理与适应变化这三个核心方面进行深入解析。每个方面都是项目成功的基石,它们相互交织,共同支撑起项目的顺利推进与最终成就。明确的目标设定与规划项目管理的首要...
建筑工程项目管理规范   0  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用