在Python中,如果我输入a=1 b=2 c=a c=b,那么c的值是什么?c指向什么?

7

Python变量大多很容易理解,但有一种情况一直让我苦恼。如果我想将我的变量指向一个新的内存地址,我该怎么做?或者说,如果Python默认这样做(将变量视为指针),那么如何将一个新变量的值文字上赋给旧变量的内存地址?

例如,如果我键入:

a=1
b=2
c=a
c=b

c的值是多少?它指向什么?这个语句是将指针c->a替换为指针c->b还是获取b的值,并将ab的值覆盖掉?c=b是含糊不清的。

换句话说,如果您从这里开始:

a -> 1 <- c
b -> 2

这是否像这样重新指向c

a -> 1    _c
b -> 2 <-/

或者像这样复制b

a -> 2 <- c
b -> 2

6
在Python中,您可以根据需要重新分配变量。现在,c引用与b相同的对象。尝试使用print(id(c)== id(b))print(id(c)== id(a))来验证。现在,c和a之间的任何相似之处都被抹去了。关于名称,请参阅:https://docs.python.org/3/reference/executionmodel.html。我相信在SO上有更详细的答案关于这个话题。 - Anton vBR
3
好的,在看到您的更新后:“name指向对象,它们并不持有任何值,没有涉及复制。” - Anton vBR
5
请阅读StackOverflow传奇人物Ned Batchelder的文章,《关于Python名称和值的事实与神话》(Facts and myths about Python names and values)来详细了解此问题。链接:https://nedbatchelder.com/text/names.html - juanpa.arrivillaga
@juanpa.arrivillaga 不错的文章。甚至还有代码运行时可视化工具。 - Ryan
@juanpa.arrivillaga 恰好是我们需要的参考。任何有兴趣理解这个问题的人都应该链接到那个页面。 - Anton vBR
8个回答

15
Python中没有变量的指针。特别地,当你这么说的时候:
“这个语句是否将指针a>替换为指针b>…”
Python没有任何“指针a>”这样的东西,所以它不会那样做。
“…或者从b获取值并用b的值覆盖a”
但是没有对a进行赋值,所以也不会那样做。
相反,Python保留了一个符号表1,将每个名称(a、b、c等)映射到一个对象的指针。在您的代码示例中,当您对a和b进行赋值后,它将如下所示(显然我已经编造了内存地址):
a -> 0xfffa9600 -> 1
b -> 0xfffa9608 -> 2

在您进行c = a赋值后,它将会变成这样:

a -> 0xfffa9600 -> 1
b -> 0xfffa9608 -> 2
c -> 0xfffa9600 -> 1
注意,c完全独立于a。当你运行 c = b时,它会用与b关联的指针替换符号表中与c关联的指针,但a不受影响。
a -> 0xfffa9600 -> 1
b -> 0xfffa9608 -> 2
c -> 0xfffa9608 -> 2

在这种情况下,由于所涉及的对象即整数常量12是不可变的,因此基本上就是这样了。但是,如果您使用可变对象,则它们会开始有些像指针,因为将对象存储在一个变量中时对对象进行的更改会反映在引用同一对象的其他变量中。例如,请考虑以下代码示例:

x = {'a': 1, 'b': 2}
y = x

在这里,符号表可能看起来像这样:

x -> 0xffdc1040 -> {'a': 1, 'b': 2}
y -> 0xffdc1040 -> {'a': 1, 'b': 2}

如果你现在运行

y['b'] = y['a']

那么它实际上不会改变符号表中与y相关联的指针,但它确实会改变该指针所指向的对象,因此你最终会得到

x -> 0xffdc1040 -> {'a': 1, 'b': 1}
y -> 0xffdc1040 -> {'a': 1, 'b': 1}

你会发现对 y['b'] 的赋值也影响了 x。与此形成对比的是:

and you'll see that your assignment to y['b'] has affected x as well. Contrast this with
y = {'a': 1, 'b': 2}

实际上有几个符号表,对应不同的作用域,Python 有一种检查它们的顺序,但这个细节在这里并不特别相关。


2这实际上使 y 指向一个完全不同的对象,更类似于您之前使用 abc 的方式。


不错!这些内存地址是否等于对象的ID? - Anton vBR
非常深入。谢谢。正如其他人指出的那样,我的解释不够清晰。当我写 c -> a 时,在我的脑海中,我正在将 a 解析为存储值为1的物理内存位置。似乎Python有一个额外的解析层。我的C++背景在Python中引起了困惑。 - Ryan
2
如果你说的“ids”是指从id()内置函数返回的值,那么它们可能是。在标准的Python解释器(CPython)中,至少目前为止,id()确实会给出内存地址。其他Python解释器(或CPython的未来版本)可能会有所不同。 - David Z
@Ryan 不用谢,很高兴能帮到你。这是许多从低级语言转来使用Python的人常见的困惑点,所以你会在Stack Overflow和其他地方找到很多相关的文章。 - David Z

7

c并不是指向a或者b,而是指向1或者2对象。

>>> a = 1
>>> b = 2
>>> c = a
>>> c
1
>>> c = b
>>> c
2
>>> b = 3
>>> c
2

这可以通过 id() 得到证明 - bc 指向同一个 "thing":

>>> b = 2
>>> c = b
>>> id(b)
42766656
>>> id(c)
42766656

1
你的回答在深度和易懂性之间取得了良好的平衡。对于像我这样的C++开发人员来说,意识到值是与标签c分离的对象至关重要。c是一个包含指向另一个内存位置(称为1)的指针的内存位置,但在C++中,c是包含数据1的内存位置。 - Ryan

2
回答你的两个问题“c的值是什么?”和“c指向什么?”,我已经添加了一个逐步执行的过程,并使用了适当的注释来显示每个变量的id()。希望这能帮助你更好地理解底层发生了什么。
>>> a=1
>>> b=2
>>> print(id(a))
1574071312    # this is the address of a
>>> print(id(b))
1574071344    # this is the address of b
>>>c=a        # assignment of a to c
>>> print(c)
1             # c will contain now the value of a
>>> print(id(c))
1574071312    # this is the address of c which is same as a
>>> c=b       # re-assignment of b to c
>>> print(c)
2             # c wil contain now the value of b  
>>> print(id(c))
1574071344    # this the address of c now which is same as b

1

请编辑您的问题,使用格式化使其更易于阅读。 - Bert Verhees
这是向新程序员解释的好方法。方括号 [ ] 的可视化非常有帮助。 - Ryan

1
以下是其他人提供的一些很好的答案总结:
1. 值是没有名称的内存位置上的对象。 2. 变量(变量名/标签)没有固有值。它们是单独的对象,在内存中有自己的空间,可以指向任何值对象。 3. 赋值运算符将标签对象指向一个值对象。
让我们不准确地从Python解释器的角度逐步进行赋值操作:
  1. First, we create a value.

    [value obj]
    

    Note: [ ] denotes a physical memory location. This means the value has its own unique memory address.

  2. Next, we create a label.

    [Label obj] -> nothing
    
  3. Last, we assign the label to its value.

    [Label obj] -> [value obj]     
    
所以,
a = 1

是等同于

[memorylocation containing "a"] -> [memorylocation containing 1]

并且

c = b

是相同的。
[memorylocation containing "c"]  ->  "b" resolved to [memorylocation containing 2]

2
我认为“变量对象”和“名称对象”只会让人感到困惑。在Python中,命名空间就是一个字典。 - juanpa.arrivillaga

0
你遇到的是 Python 中的引用重复问题。引用自 copy 模块文档

在 Python 中,赋值语句不会复制对象,它们只是在目标和对象之间创建绑定关系。

如果你从对象和它们的值的角度思考,并使用 is 运算符id() 内置函数,你可以观察到它是如何工作的。
>>> a=1
>>> b=2
>>> c=a
>>> a is c
True
>>> id(a), id(c)
(10932288, 10932288)
>>> id(a), id(c)

除此之外,你还可以通过引用计数来精确地验证相同之处:

>>> import sys
>>> a=1
>>> b=2
>>> sys.getrefcount(a)
803
>>> sys.getrefcount(b)
97
>>> c=a
>>> sys.getrefcount(c)
804
>>> sys.getrefcount(a)
804
>>> c=b
>>> sys.getrefcount(a)
803
>>> sys.getrefcount(b)
98
>>> 

顺带一提,这与深复制和浅复制有关。再次引用复制文档:
“浅复制”和“深复制”的区别仅涉及到复合对象(包含其他对象的对象,如列表或类实例):
- “浅复制”构造一个新的复合对象,然后(在可能的范围内)将原始对象中找到的对象的引用插入其中。 - “深复制”构造一个新的复合对象,然后递归地将原始对象中找到的对象的副本插入其中。
您的示例使用简单变量,并且它们始终默认为引用复制 - 无论您尝试进行深度复制,都不会创建新对象。
>>> import copy
>>> id(b),id(c)
(10932320, 10932320)
>>> c = copy.deepcopy(b)
>>> id(b),id(c)
(10932320, 10932320)

然而,如果您尝试分配元组或列表,情况就不同了:

>>> a = [1,2,3]
>>> b = [3,2,1]
>>> c = a
>>> id(a),id(c)
(139967175260872, 139967175260872)
>>> c = copy.deepcopy(a)
>>> id(a),id(c)
(139967175260872, 139967175315656)

在上面的例子中,你得到了一个完全不同的对象。为什么这可能很有用呢?简单赋值只是使两个变量引用同一个对象的事实也意味着,如果你改变其中一个,另一个也会反映出这些变化。
>>> id(c),id(a)
(139967175260872, 139967175260872)
>>> a.append(25)
>>> id(c),id(a)
(139967175260872, 139967175260872)
>>> c
[1, 2, 3, 25]
>>> 

当你想要保留原始数据时,这可能是不切实际的。当你想要最初拥有两个相同的对象,但随后让它们以自己的方式改变 - 这就是你想要只为对象本身进行浅复制或为包含在对象中的所有对象进行深复制的地方:

>>> c = copy.deepcopy(a)
>>> a.append(35)
>>> a
[1, 2, 3, 25, 35]
>>> c
[1, 2, 3, 25]

仅供演示目的,浅复制:

>>> c = a
>>> a.append([9,8,7])
>>> a
[1, 2, 3, 25, 35, [9, 8, 7]]
>>> c = a
>>> id(a), id(c), id(a[-1])
(139967175260872, 139967175260872, 139967175315656)
>>> c = copy.copy(a)
>>> id(a), id(c), id(a[-1])
(139967175260872, 139967175315528, 139967175315656)

同一主题下更好的例子请参见grc的精彩回答


0

基本上,在第四行,将c变量被b的值覆盖。由于这是最后一条语句,c将保存值2。


如何显式地强制将b的值覆盖/复制到a的值? - Ryan
2
在Python中,你不需要。变量就像对象的名称标签,它们不是内存地址。Python没有指针。 - juanpa.arrivillaga
这个解释含糊不清,因为它依赖于我已经理解“c变量”是指向另一个内存位置的自己的内存位置。 - Ryan

0

好的,在你的代码中:

a=1
b=2
c=a
c=b

在将 c 分配给 b 的值之前,以及将 c 分配给 a 的值之后,c 将是 a

然后,在代码的结尾处,c 将是 b,因为您正在重新分配变量。

第二个赋值基本上创建了一个新变量,不知道变量已经存在,所以它只会这样做,但无法访问先前保存的变量值。


谢谢。我喜欢你的答案,因为它展示了Python的执行模型与C++有多么不同。在你的解释中,当你说“将c分配给b”时,C++开发人员会看到这并认为它是反向的。他们会“将b分配给c”,因为赋值运算符是一个非常低级别的操作,具有数据的隐含流/方向(检索b的值,malloc到内存堆,并将CPU寄存器转储到c处的内存位置)。 - Ryan
从现在开始,这就是我将会思考的方式。 - Ryan
5
我认为这个回答有点误导性,因为它说了像"c将是a"这样的话,但事实上c并不是a;相反,ca恰好是同一个值。ca之间没有关系,除了在程序执行的某个特定点上它们碰巧同时引用了相同的值。如果这个解释真的帮助你理解Python的工作方式,那很好,但我认为它有可能会像你提出问题时一样,混淆其他人的思维。 - David Z
1
@Ryan,用C语言的术语来说,在Python中,你不能直接引用对象的值,只能通过一个指针间接引用,而这个指针是无法解引用的。你只能在符号表上交换指针。 - juanpa.arrivillaga
@DavidZ 这个措辞非常适合帮助低级语言程序员理解Python的混乱执行模型。他措辞中的不准确实际上有助于提示C程序员的大脑,这不是一样的。它将C程序员的大脑从C模式中推出,同时以一种对C程序员的大脑有意义的方式表达,因为C程序员的大脑会自动将ab解析为它们实际的内存位置。只有对非C程序员来说才会有误导哈哈。 - Ryan
1
@Ryan 我不同意。明确一下,我理解你在评论中所说的方式,_一些_低级程序员也会理解答案,对他们来说这将是有帮助的,但我认为其他人(特别是其他低级程序员)会误解它,对于那些人来说,这种解释方式将是有害的。我怀疑这只会误导非C程序员,正如你所声称的那样。 - David Z

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