如果两个变量指向同一个对象,为啥重新分配一个变量不会影响另一个?
Posted
技术标签:
【中文标题】如果两个变量指向同一个对象,为啥重新分配一个变量不会影响另一个?【英文标题】:If two variables point to the same object, why doesn't reassigning one variable affect the other?如果两个变量指向同一个对象,为什么重新分配一个变量不会影响另一个? 【发布时间】:2019-11-02 04:05:36 【问题描述】:我试图了解变量在 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
不再正确?有人可以解释发生了什么吗?
【问题讨论】:
不同的变量可能指向相同的内存位置,但这并不意味着它们是相同的内存位置。在重新分配时,您更改该变量指向的内容。 我用电锯回答了你的问题;如果我无意中改变了你的要求,请告诉我。 很遗憾,我们不能将其用作欺骗目标,但您必须阅读this。 【参考方案1】:在 Python 中,所有变量都存储在字典或看起来很像字典的结构中(例如,locals()
可以将当前作用域/命名空间公开为字典)。
注意:PyObject*
是一个 CPython 概念。我不确定在其他 Python 实现中是如何工作的。
因此,查看具有精确内存位置的 C 之类的 Python 变量是有缺陷的。它们的值是PyObject*
(指针或内存位置),而不是实际的原始值。由于变量本身只是字典中指向PyObject*
指针的条目,因此更改变量的值实际上是给它一个不同的内存地址来指向。
在 CPython 中,id
和 is
使用的正是这些 PyObject*
值(a 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
或类似的(来自 Python 用户区的 int
)
从一个常量内存地址加载一个值
将这两个值相加。这将产生一个新的PyObject*
,它也是一个int
将与x
关联的值设置为这个新指针
TL;DR:Python 中的一切,包括原语,都是一个对象。变量本身并不存储值,而是将它们装箱的指针。重新分配变量会更改与该名称关联的指针,而不是更新该位置中保存的内存。
【讨论】:
对 CPython 的深入了解,但我认为在不深入研究特定实现的内部结构的情况下回答这个问题会更好。我认为这对于初学者来说并不容易理解。 我相信您的回答会对从 C 语言进入 Python 或熟悉指针的人有所帮助,但这似乎不适用于 OP。【参考方案2】:“假设我在变量 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
【讨论】:
【参考方案3】:Python 有 names 引用 objects。对象与名称分开存在,名称与其所指的对象分开存在。
# 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 -+ -\
\
--> +- <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
(以及别名n
)引用的列表的内容。 m
和 n
仍然引用原始列表对象,其 值 已更改。
+- name m -+ -\
\
--> +- […] -+ +--- <@0> -+
/ | @0 | -> | 1337 |
+- name n -+ -/ +-------+ +----------+
# m += [9001]
+- name m -+ -\
\
--> +- […] -+ +--- <@0> -++--- <@1> -+
/ | @0 @1 | -> | 1337 || 9001 |
+- name n -+ -/ +-------+ +----------++----------+
【讨论】:
【参考方案4】:我用通俗的语言向您解释,以便您可以轻松理解。
案例一
a = [1, 2, 3]
b = a
print(b is a)
a
的值为[1,2,3]
。现在我们将[1,2,3]
分配给b
也由a
分配。所以两者具有相同的值,因此b is a
= True
。
下一步,
a = 'x': 'y'
print(a is b)
现在您将a
的值更改为'x':'y'
但是我们的b
仍然与[1,2,3]
相同。所以现在a is b
是False
。
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
。
希望对你有帮助。
【讨论】:
以上是关于如果两个变量指向同一个对象,为啥重新分配一个变量不会影响另一个?的主要内容,如果未能解决你的问题,请参考以下文章
Python 如何知道两个字符串变量指向同一个对象? [复制]