难以理解Python中的值和引用传递
在Python中,关于对象何时会改变、何时不会改变的问题让我有点困惑。下面是我随便编的一个例子:
class person:
age = 21
class bar:
def __init__(self, arg1):
self.foo = arg1
self.foo.age = 23
def baz(arg1):
arg1.age = 27
def teh(arg1):
arg1 = [3,2,1]
Person1 = person()
bar1 = bar(Person1)
print Person1.age
print bar1.foo.age
baz(Person1)
print Person1.age
print bar1.foo.age
meh = [1,2,3]
teh(meh)
print meh
输出结果是:
23
23
27
27
[1, 2, 3]
当我们创建Person1的时候,Person1的年龄是21。这个对象的引用被传递给了另一个类的构造函数,那个类的实例叫做bar1。对这个引用所做的任何修改都会影响到Person1。
当我们把Person1传给一个普通函数时,Person1的年龄现在变成了27。
但是,为什么在变量"meh"上就不行呢?如果我们把一个变量a = meh
,然后再改变a = [6, 6, 6]
,那么meh也会被改变。这个让我很困惑。有没有什么资料可以解释这些是怎么回事?
3 个回答
Python没有所谓的“值传递”或“引用传递”,而是采用了一种叫做“对象传递”的方式。简单来说,就是对象会直接传入函数,并和函数定义中给定的参数名绑定在一起。
比如,当你写spam = "green"时,你把名字spam和字符串对象"green"绑定在了一起;如果接着写eggs = spam,你并没有复制任何东西,也没有创建引用指针;你只是把另一个名字eggs绑定到了同一个对象上(在这个例子中就是"green")。如果之后你把spam绑定到其他东西上(比如spam = 3.14159),那么eggs依然会绑定在"green"上。
在你的teh
函数中,你并没有改变传入的对象,而是把名字arg1
重新绑定到了一个不同的列表上。要改变arg1
,你需要这样做:
def teh(arg1):
arg1[:] = [3, 2, 1]
当然,如果我们把一个变量a赋值为meh,然后把a改成[6, 6, 6],那么meh也会被改变。
其实,并不会:
>>> meh = [1,2,3]
>>> a = meh
>>> a = [6, 6, 6]
>>> print a
[6, 6, 6]
>>> print meh
[1, 2, 3]
这就是“覆盖”一个变量和“修改”一个变量所指向的实例之间的区别。
列表、字典、集合和对象都是可变类型。如果你在这些实例中添加、删除、设置、获取或以其他方式修改某些内容,那么所有引用这个实例的地方都会更新。
但是,如果你把一个完全新的实例赋值给一个变量,这样就会改变这个变量存储的引用,因此旧的引用实例不会被改变。
a = [1,2,3] # New instance
a[1] = 4 # Modifying existing instance
b = {'x':1, 'y':2} # New instance
b['x'] = 3 # Modifying existing instance
self.x = [1,2,3] # Modifying existing object instance pointed to by 'self',
# and creating new instance of a list to store in 'self.x'
self.x[0] = 5 # Modifying existing list instance pointed to by 'self.x'
我可以看到三个基本的Python概念,可以帮助理解这个问题:
1) 首先,从一个可变对象(比如在
self.foo = arg1
中的赋值)来看,就像是复制了一个指针(而不是指针指向的值):self.foo
和arg1
是同一个对象。这就是为什么接下来的那行代码,
self.foo.age = 23
会修改arg1
(也就是Person1
)。变量实际上是不同的“名字”或“标签”,可以指向一个独特的对象(在这里是一个person
对象)。这就解释了为什么baz(Person1)
会把Person1.age
和bar1.foo.age
都改成27,因为Person1
和bar1.foo
只是同一个person对象的两个名字(在Python中,Person1 is bar1.foo
返回True
)。
2) 第二个重要的概念是赋值。在
def teh(arg1):
arg1 = [3,2,1]
中,变量arg1
是局部的,所以代码
meh = [1,2,3]
teh(meh)
首先执行arg1 = meh
,这意味着arg1
是列表meh
的一个额外(局部)名字;但是执行arg1 = [3, 2, 1]
就像是在说“我改变主意了:arg1
现在将是一个新的列表,[3, 2, 1]”。这里要记住的重要一点是,赋值虽然用“等号”表示,但它是不对称的:它给右边的(可变)对象一个额外的名字,左边的名字就是这个额外的名字(所以你不能写[3, 2, 1] = arg1
,因为左边必须是一个名字)。因此,arg1 = meh; arg1 = [3, 2, 1]
不会改变meh
。
3) 最后一点与问题标题有关:“按值传递”和“按引用传递”在Python中并不相关。相关的概念是可变对象和不可变对象。列表是可变的,而数字则不是,这解释了你观察到的现象。此外,你的Person1
和bar1
对象是可变的(这就是你可以改变人的年龄的原因)。你可以在文本教程和视频教程中找到更多关于这些概念的信息。维基百科也有一些(更技术性)的信息。一个例子说明了可变和不可变之间的行为差异:
x = (4, 2)
y = x # This is equivalent to copying the value (4, 2), because tuples are immutable
y += (1, 2, 3) # This does not change x, because tuples are immutable; this does y = (4, 2) + (1, 2, 3)
x = [4, 2]
y = x # This is equivalent to copying a pointer to the [4, 2] list
y += [1, 2, 3] # This also changes x, because x and y are different names for the same (mutable) object
最后一行并不等同于y = y + [1, 2, 3]
,因为这只会把一个新的列表对象放入变量y
中,而不是改变y
和x
都指向的列表。
以上三个概念(变量作为名字[对于可变对象]、不对称赋值、可变性/不可变性)解释了Python的许多行为。