如果两个变量指向同一个对象,为什么重新分配一个变量不会影响另一个变量?
- 2025-03-26 09:10:00
- admin 原创
- 13
问题描述:
我试图理解变量在 Python 中的工作方式。假设我在变量中存储了一个对象a
:
>>> a = [1, 2, 3]
如果我分配a
给b
,两者都指向同一个对象:
>>> b = a
>>> b is a
True
但如果我重新分配a
或b
,情况就不再如此:
>>> a = {'x': 'y'}
>>> a is b
False
这两个变量现在具有不同的值:
>>> a
{'x': 'y'}
>>> b
[1, 2, 3]
我不明白为什么变量现在不同了。为什么a is b
不再正确?有人能解释一下发生了什么吗?
解决方案 1:
Python 中的名称指的是对象。对象独立于名称而存在,名称也独立于其所引用的对象而存在。
# name a
a = 1337
# object 1337
当将“名称赋给名称”时,右侧将计算为所引用的对象。与2 + 2
计算为类似4
,a
计算为原始1337
。
# name b
b = a
# object referred to by a -> 1337
此时,我们有a -> 1337
和b -> 1337
- 请注意,两个名字都不认识对方!如果我们测试a is b
,两个名称将被评估为同一个对象,这显然是相等的。
重新分配名称只会改变该名称所指的内容 - 不存在可以更改其他名称的联系。
# name a - reassign
a = 9001
# object 9001
此时,我们有a -> 9001
和b -> 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 -+ -\n \n --> +- <12> ---+
/ | 12 |
+- name b -+ -/ +----------+
# a = -128
+- <-128> -+
+- name a -+ -----> | -128 |
+----------+
+- <12> ---+
+- name b -+ -----> | 12 |
+----------+
容易让人混淆的是,可变对象似乎违反了名称和对象的分离。通常,这些是容器(例如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
`nm
n`
+- name m -+ -\n
--> +- […] -+ +--- <@0> -+
/ | @0 | -> | 1337 |
+- name n -+ -/ +-------+ +----------+
# m += [9001]
+- name m -+ -\n
--> +- […] -+ +--- <@0> -++--- <@1> -+
/ | @0 @1 | -> | 1337 || 9001 |
+- name n -+ -/ +-------+ +----------++----------+
解决方案 2:
在 Python 中,所有变量都存储在字典或看起来很像字典的结构中(例如,locals()
可以将当前范围/命名空间显示为字典)。
注意:PyObject*
是 CPython 概念。我不确定在其他 Python 实现中情况如何。
因此,将 Python 变量视为具有精确内存位置的 C 变量是错误的。它们的值是PyObject*
(指针或内存位置),而不是实际的原始值。由于变量本身只是指向PyObject*
指针的字典中的条目,因此更改变量的值实际上是为其提供了一个不同的内存地址。
在 CPython 中,这些值被和PyObject*
使用(与 相同)。id
`isa is b
id(a) == id(b)`
例如,让我们考虑一下简单的代码行:
# x: int
x += 1
实际上改变了与变量关联的内存位置。这是因为它遵循以下逻辑:
LOAD_FAST (x)
LOAD_CONST (1)
INPLACE_ADD
STORE_FAST (x)
字节码大致如下:
查找 x 的值。在 CPython 中,它是
PyObject*
指向PyLongLong
或 这样的(int
来自 Python 用户空间)从常量内存地址加载值
将两个值相加。这将产生一个新的
PyObject*
,它也是一个int
将与之关联的值设置
x
为这个新指针
TL;DR:Python 中的所有内容(包括基元)都是对象。变量本身并不存储值,而是存储封装它们的指针。重新分配变量会更改与该名称关联的指针,而不会更新保存在该位置的内存。
解决方案 3:
“假设我在变量 a 中存储了一个对象”——这就是你错的地方。
Python 对象不存储在变量中,而是通过变量引用。
a = [1, 2, 3]
b = a
a
并b
引用同一个对象。该list
对象的引用计数为 2,因为有两个名称引用它。
a = {'x': 'y'}
a
不再引用同一个list
对象,而是引用一个dict
对象。这会减少该对象的引用计数list
,但b
仍引用该对象,因此该对象的引用计数现在为 1。
b = None
这意味着b
现在引用了None
对象(该对象的引用计数非常高,许多名称都引用了None
)。该list
对象的引用计数再次减少,并降至零。此时,该list
对象可以被垃圾回收,内存被释放(何时发生这种情况无法保证)。
参见sys.getrefcount
解决方案 4:
我用外行人能理解的语言向你解释,以便你能够轻松理解。
案例1
a = [1, 2, 3]
b = a
print(b is a)
的值a
是[1,2,3]
。现在我们将也[1,2,3]
赋给。因此两者具有相同的值,因此 = 。b
`ab is a
True`
下一步,
a = {'x': 'y'}
print(a is b)
现在您将 的值更改a
为 ,{'x':'y'}
但我们的b
值仍然与 相同[1,2,3]
。因此现在a is b
是False
。
案例 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
我希望这对你有帮助。