难以理解Python中的值和引用传递

3 投票
3 回答
1300 浏览
提问于 2025-04-17 04:42

在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 个回答

0

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]
6

当然,如果我们把一个变量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'
8

我可以看到三个基本的Python概念,可以帮助理解这个问题:

1) 首先,从一个可变对象(比如在

self.foo = arg1

中的赋值)来看,就像是复制了一个指针(而不是指针指向的值):self.fooarg1同一个对象。这就是为什么接下来的那行代码,

self.foo.age = 23

会修改arg1(也就是Person1)。变量实际上是不同的“名字”或“标签”,可以指向一个独特的对象(在这里是一个person对象)。这就解释了为什么baz(Person1)会把Person1.agebar1.foo.age都改成27,因为Person1bar1.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中并不相关。相关的概念是可变对象不可变对象。列表是可变的,而数字则不是,这解释了你观察到的现象。此外,你的Person1bar1对象是可变的(这就是你可以改变人的年龄的原因)。你可以在文本教程视频教程中找到更多关于这些概念的信息。维基百科也有一些(更技术性)的信息。一个例子说明了可变和不可变之间的行为差异:

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中,而不是改变yx都指向的列表。

以上三个概念(变量作为名字[对于可变对象]、不对称赋值、可变性/不可变性)解释了Python的许多行为。

撰写回答