只是想知道这背后的逻辑是什么?从表面上看,这似乎有点低效,每次你做一些简单的事情,比如“x=x+1”,它都必须采用一个新地址并丢弃旧地址。
5 回答
Python 变量(在 Python 中称为标识符或名称)是对值的引用。该id()
函数说明了该值,而不是名称。
许多值是不可变的;整数、字符串、浮点数都不会原地改变。当您添加1
到另一个整数时,您返回一个新整数,然后替换对旧值的引用。
您可以将 Python 名称视为与值相关联的标签。如果您将值想象为气球,则每次分配给该名称时,您都在为标签重新分配一个新气球。如果气球上没有其他标签,它就会随风飘走,再也见不到了。该id()
函数为您提供该气球的唯一编号。
请参阅我以前的答案,其中我更多地讨论了值作为气球的想法。
这可能看起来效率低下。对于许多经常使用的小值,Python 实际上使用了一个称为 interning 的过程,它会缓存这些值的存储以供重复使用。None
是这样一个值,小整数和空元组 ( ()
) 也是如此。您可以使用该intern()
函数对您希望经常使用的字符串执行相同的操作。
但请注意,只有当它们的引用计数(“标签”的数量)降至 0 时,才会清理值。大量的值一直在各处重复使用,尤其是那些内部整数和单例。
因为基本类型是不可变的,所以每次修改都需要再次实例化
...这非常好,尤其是对于线程安全函数
=
操作员不会修改对象,它会将名称分配给完全不同的对象,该对象可能已经或可能没有 id 。
对于您的示例,整数是不可变的;没有办法在其中添加一些东西并保持相同的 id。
而且,事实上,小整数至少在 cPython 中是被实习的,所以如果你这样做:
x = 1
y = 2
x = x + 1
然后x
和y
可能有相同的id。
在 python 中,像整数和字符串这样的“原始”类型是不可变的,这意味着它们不能被修改。
Python 实际上非常高效,因为正如@Wooble评论的那样,«非常短的字符串和小整数被保留。»:如果两个变量引用相同的(小)不可变值,则它们的 id 相同(减少重复的不可变值)。
>>> a = 42
>>> b = 5
>>> id(a) == id(b)
False
>>> b += 37
>>> id(a) == id(b)
True
使用不可变类型背后的原因是对这些值的并发访问的安全方法。
归根结底,这取决于设计选择。
根据您的需要,您可以更多地利用一个实现而不是另一个。
例如,在一种有点相似的语言 Ruby 中可以找到不同的哲学,其中那些在 Python 中是不可变的类型不是。
准确地说,赋值x=x+1
不会修改x
引用的对象,它只是让 x 指向另一个值为 的对象x+1
。
要理解背后的逻辑,需要了解值语义和引用语义之间的区别。
具有值语义的对象只意味着它的值很重要,而不是它的身份。而具有引用语义的对象专注于其身份(在 Python 中,身份可以从 中返回id(obj)
)。
通常,值语义意味着对象的不变性。或者相反,如果一个对象是可变的(即就地更改),这意味着它具有引用语义。
让我们简要解释一下这种不变性背后的基本原理。
具有引用语义的对象可以就地更改而不会丢失其原始地址/身份。这是有道理的,因为具有引用语义的对象的身份使自己与其他对象区分开来。
相反,具有价值语义的对象永远不应该改变自己。
首先,这在理论上是可能的,也是合理的。由于只有值(而不是其身份)是重要的,因此当需要更改时,将其交换为具有不同值的另一个身份是安全的。这称为参照透明性。请注意,这对于具有引用语义的对象是不可能的。
其次,这在实践中是有益的。正如 OP 所认为的,每次更改旧对象时丢弃旧对象似乎效率低下,但大多数时候它比不更有效。一方面,Python(或任何其他语言)具有实习生/缓存方案来减少要创建的对象。更重要的是,如果价值语义的对象被设计为可变的,那么在大多数情况下它会占用更多的空间。
例如,Date 具有值语义。如果它被设计为可变的,任何从内部字段返回日期的方法都会将句柄暴露给外界,这是有风险的(例如,外部可以直接修改这个内部字段而无需求助于公共接口)。类似地,如果通过引用某个函数/方法来传递任何日期对象,则可以在该函数/方法中修改该对象,这可能与预期不同。为了避免这些副作用,必须进行防御性编程: 他不是直接返回内部日期字段,而是返回它的一个克隆;他不是通过引用传递,而是通过值传递,这意味着制作了额外的副本。正如人们可以想象的那样,有更多的机会创建比必要的更多的对象。更糟糕的是,这些额外的克隆使代码变得更加复杂。
总之,不变性强化了价值语义,它通常涉及更少的对象创建,更少的副作用和更少的麻烦,并且更易于测试。此外,不可变对象本质上是线程安全的,这意味着在多线程环境中锁更少,效率更高。
这就是为什么像数字、字符串、日期、时间这样的值语义的基本数据类型都是不可变的(嗯,C++ 中的字符串是一个例外,这就是为什么有这么多const string&
东西可以避免字符串被意外修改)。Date
作为一个教训,Java 在将值语义类, Point
, Rectangle
,设计Dimension
为可变时犯了错误。
众所周知,OOP 中的对象具有三个特征:状态、行为和身份。具有值语义的对象不是典型的对象,因为它们的身份根本不重要。通常它们是被动的,主要用于描述其他真实的、主动的对象(即具有引用语义的对象)。这是区分值语义和引用语义的一个很好的提示。