如果两个变量指向同一个对象,为什么重新分配一个变量不会影响另一个变量?

6

我想了解Python中变量的工作原理。假设我有一个存储在变量a中的对象:

>>> a = [1, 2, 3]

如果我将a分配给b,则两者都指向同一个对象:

>>> b = a
>>> b is a
True

但是如果我重新指定ab,那么这个说法就不再成立:

>>> a = {'x': 'y'}
>>> a is b
False

现在这两个变量拥有不同的值:

>>> a
{'x': 'y'}
>>> b
[1, 2, 3]

我不明白为什么变量现在不同。 为什么 a is b 不再为真?有人能解释一下发生了什么吗?

3
不同的变量可能会“指向”同一个内存位置,但这并不意味着它们是同一个内存位置。在重新分配时,您更改了该变量“指向”的内容。 - MisterMiyagi
1
我用电锯回答了你的问题;如果我无意中改变了你的问题,请告诉我。 - Aran-Fey
6
很遗憾,我们不能将其用作重复目标,但您必须阅读此链接:https://nedbatchelder.com/text/names.html。 - Andras Deak -- Слава Україні
为什么重新分配一个变量不会影响另一个变量?出于同样的原因,当您使第二个变量引用与第一个相同的对象时,这也不会影响第二个变量可能在之前所引用的任何内容(如果它没有引用任何内容,则不会引发异常)。 - Karl Knechtel
4个回答

18

Python有指向对象的名称。对象与名称是分开存在的,名称也与其所指向的对象是分离的。

# name a
a = 1337
    # object 1337
当为“名称分配名称”时,右侧将评估所引用的对象。类似于2 + 2求值为4a求值为原始1337
# name b
b = a
    # object referred to by a -> 1337

现在,我们有 a -> 1337b -> 1337 - 注意,这两个名称彼此不知道!如果我们测试 a is b,那么这两个名称都会被评估为相同的对象,显然是相等的。

重新分配一个名称只会改变该名称所指的内容 - 没有其他名称可以通过连接来进行更改。

# name a - reassign
a = 9001
  # object 9001

现在,我们有a -> 9001b -> 1337。如果我们现在测试a is b,则两个名称都被评估为不同的对象,它们不相同。


如果您来自像C这样的语言,则习惯于变量包含值。例如,char a = 12可以解读为"a是一个包含12的内存区域"。此外,您可以让多个变量使用相同的内存。将另一个值分配给变量会更改共享内存的内容-因此也更改了两个变量的值。

+- char a -+
|       12 |
+--char b -+

# a = -128

+- char a -+
|     -128 |
+--char b -+

这并不是 Python 的工作方式:变量名并不包含任何内容,而是指向各自的值。例如,a = 12 可以理解为“a 是一个指向值为 12 的变量名”。此外,你可以拥有多个变量名指向同一个值 - 但它们仍然是不同的变量名,每个都有自己的引用。将另一个值分配给变量名会更改该变量名的引用 - 但不会影响其他变量名的引用。

+- name a -+ -\
               \
                --> +- <12> ---+
               /    |       12 |
+- name b -+ -/     +----------+

# a = -128
                    +- <-128> -+
+- name a -+ -----> |     -128 |
                    +----------+

                    +- <12> ---+
+- name b -+ -----> |       12 |
                    +----------+
一个令人困惑的点是,可变对象似乎违反了名称和对象的分离。通常,这些是容器(例如listdict等),类默认表现出相同的行为。
一个容易混淆的地方是,可变的对象看起来违反了名称和对象的分离原则。通常来说,这些对象是容器(如list, dict等),而且类默认也表现出相同的行为。
# name m
m = [1337]
    # object [1337]
# name n
n = m
    # object referred to by m
与普通整数1337类似,包含整数[1337]的列表是一个可以被多个独立名称引用的对象。就像上面一样,n is m的结果为True,而m = [9001]不会改变n的值。

然而,对某些名称的特定操作将更改和所有别名看到的值

# inplace add to m
m += [9001]

执行此操作后,m == [1337, 9001] 并且 n is m 仍然成立。实际上,n 看到的值也已经更改为 [1337, 9001]。这似乎违反了上面的行为,其中别名不会相互影响。

这是因为 m += [9001] 没有更改 m 所引用的内容,它只更改了 m(和别名 n)所引用的列表的 内容mn 仍然引用原始的列表对象,其已更改。

+- name m -+ -\
               \                  
                --> +- […] -+     +--- <@0> -+
               /    |    @0 |  -> |     1337 |
+- name n -+ -/     +-------+     +----------+

# m += [9001]

+- name m -+ -\
               \                  
                --> +- […] -+     +--- <@0> -++--- <@1> -+
               /    | @0 @1 |  -> |     1337 ||     9001 |
+- name n -+ -/     +-------+     +----------++----------+

如果我早些遇到这个答案,就可以省去两天令人沮丧的调试了!感谢您以易于理解、清晰明了的方式澄清了这个话题! - Lucas Schwartz

4
在Python中,所有变量都存储在字典中,或者类似于字典的结构中(例如,locals()可以将当前的作用域/命名空间公开为一个字典)。需要注意的是:PyObject*是CPython概念。我不确定其他Python实现的工作方式。因此,将Python变量视为具有精确内存位置的C语言变量是有缺陷的。它们的值是PyObject*(指针或内存位置),而不是实际的基本值。由于变量本身只是指向PyObject*指针的字典条目,因此改变变量的值实际上是给它指向不同内存地址的指针。在CPython中,这些PyObject*值被idis使用(a is bid(a) == id(b)相同)。例如,让我们考虑以下简单的代码行:
# x: int
x += 1

实际上改变了与变量关联的内存位置。这是因为它遵循以下逻辑:

LOAD_FAST (x)
LOAD_CONST (1)
INPLACE_ADD
STORE_FAST (x)

这段代码的字节码大致是:

  1. 查找变量 x 的值。在CPython中,x 是一个指向 PyLongLong 或者类似 PyLongLong 的 PyObject*(Python用户空间中的 int)。

  2. 从常量内存地址加载一个值。

  3. 将这两个值相加。结果会得到一个新的 PyObject*,也是一个 int。

  4. 将与 x 相关联的值设置为这个新指针。

TL;DR: Python 中的所有东西,包括基本类型,都是对象。变量本身不存储值,而是存储封装了值的指针。重新赋值变量会改变与该名称相关联的指针,而不是更新该位置上的内存。


3
对CPython的深入洞察很好,但我认为回答问题时最好不要深入探讨特定实现的内部细节。我不认为这对初学者容易理解。 - Aran-Fey
1
我相信你的回答对于从C语言转到Python或者熟悉指针的人会很有帮助,但是这似乎并不适用于原帖作者。 - PM 2Ring
很好的解释如何进入Cpython以及指针在内存位置上的作用。但我猜,"变量被赋值给值而不是复制值"这句话是对https://nedbatchelder.com/text/names1.html中这个主题的精确回答。 - xlmaster

4
"假设我有一个存储在变量a中的对象" - 这就是你犯了错误。
Python对象并不是存储在变量中的,而是被变量引用的。
a = [1, 2, 3]
b = a

ab指向同一个对象。由于有两个名字引用了它,所以list对象的引用计数为2。

a = {'x': 'y'}

a 不再引用同一个 list 对象,它现在引用一个 dict 对象。这将减少 list 对象的引用计数,但是 b 仍然引用该对象, 因此该对象的引用计数现在为 1。

b = None

这意味着b现在引用了None对象(有很多名称引用None,因此其引用计数非常高)。list对象的引用计数再次被减少,降为零。此时list对象可以被垃圾回收并释放内存(回收时间不保证)。
另请参见sys.getrefcount

0

我用通俗易懂的语言来解释,让你更容易理解。

情况一

a = [1, 2, 3]
b = a
print(b is a)

a的值为[1,2,3]。现在我们通过a[1,2,3]赋值给b。因此两者具有相同的值,因此b is a = True

下一步,

a = {'x': 'y'}
print(a is b) 

现在你正在将 a 的值更改为 {'x':'y'}但是我们的 b 仍然与 [1,2,3] 相同。因此现在 a is bFalse案例-2 如果你已经完成了以下操作:
a = [1, 2, 3]
b = a
print(b is a)
a = {'x': 'y'}
b = a  # Reassigning the value of b.
print(a is b)

在重新分配a的值之后,我还重新分配了b的值。 因此,在两种情况下你都会得到True

希望这可以帮助你。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接