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

If two variables point to the same object, why doesn't reassigning one variable affect the other?

我正在尝试了解变量在 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 不再正确?谁能解释一下这是怎么回事?

在Python中,所有变量都存储在字典中,或者是看起来很像字典的结构(例如locals()可以将当前的scope/namespace公开为字典)。

注意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 中)PyObject*,它指向 PyLongLong 或类似的(来自 Python 用户区的 int

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

  3. 将这两个值相加。这将产生一个新的 PyObject*,它也是一个 int
  4. 将与 x 关联的值设为这个新指针

TL;DR:Python 中的一切,包括原语,都是对象。变量本身不存储值,而是存储它们的指针。重新分配变量会更改与该名称关联的指针,而不是更新该位置的内存。

"假设我有一个对象存储在变量 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

Python 有 names 引用 objects。对象与名称分开存在,名称与它们所引用的对象分开存在。

# name a
a = 1337
    # object 1337

当分配"a name to a name"时,right-hand端被计算到被引用的对象。类似于 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 |
                    +----------+

一个令人困惑的地方是 mutable 对象似乎违反了名称和对象的分离。通常,这些容器(例如 listdict、...)和 类 默认情况下表现出相同的行为。

# name m
m = [1337]
    # object [1337]
# name n
n = m
    # object referred to by m

与普通整数 1337 类似,包含整数 [1337] 的列表是 一个对象 ,可以由多个独立名称引用。如上,n is m 计算为 Truem = [9001] 不会改变 n.

但是,对名称的某些操作会更改名称 和所有别名 .

看到的值
# inplace add to m
m += [9001]

这样操作后,m == [1337, 9001]n is m仍然成立。其实n看到的值也变成了[1337, 9001]。这似乎违反了上述行为,其中别名不会相互影响。

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

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

# m += [9001]

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

我用通俗易懂的语言给你解释,让你看得懂

案例一

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

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

希望对您有所帮助。