对象和基本类型的赋值
这里有一段代码:
# assignment behaviour for integer
a = b = 0
print a, b # prints 0 0
a = 4
print a, b # prints 4 0 - different!
# assignment behaviour for class object
class Klasa:
def __init__(self, num):
self.num = num
a = Klasa(2)
b = a
print a.num, b.num # prints 2 2
a.num = 3
print a.num, b.num # prints 3 3 - the same!
问题:
- 为什么赋值操作符在基本类型和类对象上表现得不一样(基本类型是按值复制,而类对象是按引用复制)?
- 怎么才能只按值复制类对象?
- 如何像在C++中那样为基本类型创建引用,比如 int& b = a?
4 个回答
假设你和我有一个共同的朋友。如果我决定不再喜欢她,她依然是你的朋友。另一方面,如果我送她一份礼物,那你的朋友也收到了礼物。
在Python中,赋值并不会复制任何东西,而“引用复制”这个说法有点尴尬,甚至没什么意义(正如你在评论中提到的)。赋值只是让一个变量开始指向某个值。Python里没有独立的“基本类型”;虽然有些是内置的,但int
实际上也是一个类。
在这两种情况下,赋值让变量指向右边表达式计算出的结果。你看到的行为正是这个环境下应该预期的,就像上面的比喻一样。无论你的“朋友”是int
还是Klasa
,给一个属性赋值和把变量重新指向一个完全不同的实例是根本不同的操作,行为也会有所不同。
唯一真正的区别是,int
并没有你可以赋值的属性。(这就是实现上需要做一些小魔法来限制你的地方。)
你把“引用”这两个不同的概念搞混了。在C++中,T&
是一种神奇的东西,当你给它赋值时,会直接更新它所指向的对象,而不是引用本身;一旦引用初始化后,它就不能再指向其他东西。这在大多数东西都是值的语言中很有用。而在Python中,一开始所有东西都是引用。Python的引用更像是一个始终有效、永远不为零、不能用于算术运算、自动解引用的指针。赋值会让这个引用开始指向一个完全不同的东西。你不能通过完全替换来“就地更新被引用的对象”,因为Python的对象就是不这样工作的。当然,你可以通过操作它的属性来更新它的内部状态(如果有可访问的属性),但这些属性本身也是引用。
引用自 数据模型
对象是Python对数据的抽象。在Python程序中,所有的数据都是通过对象或者对象之间的关系来表示的。(从某种意义上说,按照冯·诺依曼的“存储程序计算机”模型,代码也被视为对象。)
从Python的角度来看,基本数据类型和C/C++是完全不同的。它用于将 C/C++
的数据类型映射到Python中。因此,暂时不讨论这个问题,我们先考虑所有数据都是对象,并且是某个类的表现形式。每个对象都有一个ID(有点像地址)、一个值和一个类型。
所有对象都是通过引用来复制的。例如:
>>> x=20
>>> y=x
>>> id(x)==id(y)
True
>>>
创建一个新实例的唯一方法就是直接创建一个。
>>> x=3
>>> id(x)==id(y)
False
>>> x==y
False
这听起来可能有点复杂,但为了简化一下,Python让一些类型是不可变的。例如,你不能直接改变一个 字符串
。你必须对它进行切片,然后创建一个新的字符串对象。
通过引用复制常常会导致意想不到的结果。例如:
x=[[0]*8]*8
可能让你觉得它创建了一个包含 0
的二维列表。但实际上,它创建的是同一个列表对象的引用列表。因此,修改 x[1][1] 会同时改变所有的重复实例。
Copy 模块提供了一种叫做 deepcopy 的方法,可以创建对象的新实例,而不是一个浅层实例。这在你想要有两个不同的对象并分别操作时非常有用,就像你在第二个例子中所想要的那样。
为了扩展你的例子:
>>> class Klasa:
def __init__(self, num):
self.num = num
>>> a = Klasa(2)
>>> b = copy.deepcopy(a)
>>> print a.num, b.num # prints 2 2
2 2
>>> a.num = 3
>>> print a.num, b.num # prints 3 3 - different!
3 2
这对很多使用Python的人来说是个难题。Python中的对象引用方式和C语言程序员习惯的方式不太一样。
先说第一个例子。当你写 a = b = 0
时,实际上是创建了一个值为 0
的新整数对象,并且有两个引用指向它(一个是 a
,另一个是 b
)。这两个变量指向的是同一个对象(就是我们创建的那个整数)。接着,当我们执行 a = 4
时,又创建了一个值为 4
的新整数对象,a
现在指向这个新对象。这就意味着,指向 4
的引用数量是1,而指向 0
的引用数量减少了1。
再对比一下C语言中的 a = 4
,在C中,a
“指向”的内存区域会被写入。a = b = 4
在C中意味着会在两块内存中写入 4
——一块给 a
,另一块给 b
。
接下来是第二个例子,a = Klass(2)
创建了一个类型为 Klass
的对象,并将它的引用计数加1,然后让 a
指向这个对象。b = a
只是让 b
指向 a
所指向的同一个对象,并将这个对象的引用计数再加1。这和 a = b = Klass(2)
的效果是一样的。打印 a.num
和 b.num
的结果是一样的,因为你们都在访问同一个对象的属性值。你可以使用 id
这个内置函数来查看这两个对象是相同的(id(a)
和 id(b)
会返回相同的标识符)。现在,如果你通过给其中一个属性赋值来改变这个对象的内容,由于 a
和 b
都指向同一个对象,你会发现无论是通过 a
还是 b
访问这个对象时,变化都是可见的。这正是实际情况。
接下来,关于你问题的答案。
- 赋值操作符在这两者之间并没有不同。它的作用只是增加对右侧值的引用,并让左侧的变量指向它。它总是“通过引用”进行操作(虽然这个术语在参数传递的上下文中更有意义,而不是简单的赋值)。
- 如果你想要对象的“副本”,可以使用 copy模块。
- 正如我在第一点中提到的,当你进行赋值时,总是移动引用。除非你特别要求,否则不会进行复制。