The website uses cookies. By using this site, you agree to our use of cookies as described in the Privacy Policy.
I Agree

python中的赋值,什么时候是传值什么时候是传址?

s = [1, 2, 3] t = s t.reverse() 然后s和t都变成了[3, 2, 1] 但是如果s = [1, 2, 3] t = s[…
被浏览
5,817
清华大学 计算机科学与技术硕士

参数的传递是通过自动将对象赋值给本地变量名来实现的。在函数运行时,函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内存在。参数的传递本质上就是python赋值的另一个实例而已。

那么,这个问题分为可变对象和不可变对象两种情况进行讨论:

在原处改变函数的可变对象参数的值会对调用者有影响。函数能够就地改变传入的可变对象,因此其结果会影响调用者,这其实和前面介绍过的对象赋值原理是一样的;

而不可变对象的引用重新赋值会指向新的对象,因此不会改变调用者。

先看一个不可变对象的例子

def f(a):
    a = 99
    print(a)

b = 88
f(b)
    print(b)

99
88

在函数中修改a对于调用函数的地方没有任何影响,因为他在函数内部直接把本地变量a重置为了一个完全不同的新对象,所以他不会影响最初的变量b

而当参数传递像列表和字典这样的可修改对象的时候,我们还需要注意,对这样的可变对象的原处修改可能在函数退出后依然有效,并由此影响到调用者。

def change(a,b):
    a = 2
    b[0] = 'spam'

x = 1
l = [1,2]
change(x,l)
print(x,l)

1 ['spam', 2]

再次对比可以看出,调用者的不可变变量x没有受到影响,而可变变量L在函数内部进行本地修改,并影响到了自身

可以看出,对于参数a,仅仅把本地变量a修改为引用一个完全不同的对象,并没有改变调用者作用域中的名称x的绑定。而参数b被传给了一个可变对象(在调用者作用域中叫做L的列表),因为第二个赋值是一个在原处发生的对象改变,对函数中b[0]进行赋值的结果会在函数返回后影响L的值,他修改了b所引用的对象的一部分,因为引用共享对象的缘故,L也被同时改变。

再强调一次,其实参数传递后的本地修改过程和简单对象赋值后的对象修改,实质上是一回事,换句话说就等于下面这个例子所描述的程序过程

L = [1,2]
b = L
b[0] = 'spam'
print(L)

['spam', 2]

那如何避免对可变参数的修改呢?

实际上,可变参数的原处修改行为并不是一个bug,它只是参数传递在python中工作的方式。在python中,默认通过引用进行函数的参数传递,是因为这通常是我们所想要的:这意味着不需要创建多个拷贝就可以在我们的程序中传递很大的对象。如果不想要函数内部在原处的修改影响传递给它的对象,那么,我们可以简单的创建一个明确的可变对象的拷贝

def change(a,b):
    a = 2
    b[0] = 'spam'

x = 1
l = [1,2]
change(x,l[:])
print(x,l)

1 [1, 2]

或者在函数内部进行拷贝,这样可以不改变传入的对象,函数调用看上去没有变化

def change(a,b):
    b = b[:]
    a = 2
    b[0] = 'spam'

x = 1
l = [1,2]
change(x,l)
print(x,l)

1 [1, 2]

更系统、更深入的探讨,可点击进入我们的专栏《python数据科学之路》。

真诚赞赏,手留余香
还没有人赞赏,快来当第一个赞赏的人吧!
继续浏览内容
知乎
发现更大的世界
打开
Chrome
继续

更多回答

兔子们,虾米们,不要酱瓜,咸菜太贵了!
Python一切皆为对象。赋值一直都是传址。所有变量都是保存着对象的地址。

首先,分析一下题主所描述的两种情况出现不同的原因。
第一种情况将s赋值给了t,此时s和t指向了同一个对象。所以执行reverse时,对象本身被改变。因为s和t指向同一个对象,所以你无论输出s还是t都是输出同一个已经被reverse的对象。
第二种情况是对s执行了一个slicing的操作。此时本身s[::-1]返回的不是s对象本身,而是一个在内存中根据运算重新生成的对象,所以t得到的是一个s[::-1]生成的新对象的地址。而s还是保留着原来的对象,由于s[::-1]不会改变原来对象的值,所以s的值是不会改变的。

再进一步。

在Python中,即使是整数类型,它也是按照对象来处理的。例如a=1,它并不是将1值赋值给了a,而是将一个整数对象1的地址赋值给了a。由于Python对小整数的特殊处理,凡是在一定范围内的小整数,是统一使用了“小整数对象池”。也就是说所有的小整数,例如1,都是使用对象池里面的同一个对象。但是,小整数对象池是有限的,范围是[-5, 257) 注意左闭右开。所以,超过这个范围的整数,严格来说,是需要生成这样的一个对象的。所以,就会出现下面的情况
>>> a = 1
>>> b = 1
>>> id(a) == id(b)
True

>>> c = 1000000
>>> d = 1000000
>>> id(c) == id(d)
False
而整数对象是一种不可变类型,也就是说,一旦你生成了一个257的整形对象,你这个对象保存的数字就是不能再变化的了。那么我们对整数执行加法的时候,得到的结果和原来的对象是什么关系呢?答案是,没关系,结果是根据求和数值产生的一个全新的对象。即使全新的对象和原来数值一样,也是不同的对象(除非数字在小整数对象池内)。例如:
>>> e = c + 0
>>> id(c) == id(e)
False
>>> f = a + 0
>>> id(a) == id(f)
True
而列表类型是一种可变类型。他提供了一些原地改变对象而不用生成新对象的方法,例如题主的s.reverse()。但同时,也可以生成一个新的对象储存想要的结果,例如题主的s[::-1],或者reversed(s)。PS:注意reverse()和reversed的区别

以上!
=========
Updates
2014-11-16
提醒,修改了一个笔误。
2014-11-18
提醒,修改了一个笔误。
=========
参考资料
Python 源码阅读
Python源码--整数对象(PyIntObject)的内存池
继续浏览内容
知乎
发现更大的世界
打开
Chrome
继续
是时候了/你在说啥?
在python中, parameter sent to function 使用的全部是 by object。
也就是,这无法通过by value或者 by reference 来定义。这是python的独到之处。
如果object本身是immutable的,例如一个不是太长的整数,那么你可以看作是传值。因为每一次对这个object赋值,都会创建一个新的object,如下:
a=10

def function1(value):
value=20
print(value)

function1(a)

print(a)
结果是
20
10
虽然传过去的是a这个object,但当function1对a赋值的时候,其实他并没有改变a,而是创建了一个新的object,这个object叫做value了。global当中的a并没有变。

如果object本身是mutable,例如一个list,因为每一次对这个object赋值,都会改变这个object本身。那么就可以看作是传reference。如下:
a=[10,11,12,13]

def function1(value):
value[1:3]=[]
print(value)

function1(a)

print(a)
结果是
[10,13]
[10,13]
。。答到一半,看了下题目好像答非所问了。

题目问的问题其实更简单。
list.reverse 是一个in-place method。也就是说,reverse是在原来object上操作,而不会创造一个新的object。上面t=s,按照python传object的标准,那么就是t=s 是同一个object。.reverse作用在这个object上,那么t,s都变了。他们只是名字而已。

而slicing [::] 这个,会创造一个新的object。所以。自然啦。
最好的办法是deep copy
继续浏览内容
知乎
发现更大的世界
打开
Chrome
继续
Measure
Measure
Summary | 4 Annotations
一种不可变类型
2021/02/08 04:28
你这个对象保存的数字就是不能再变化的了
2021/02/08 04:28
列表类型是一种可变类型
2021/02/08 04:28
原地改变对象而不用生成新对象的方法
2021/02/08 04:28