传入函数的列表执行`my_list *=2`与`my_list = my_list *2`为何结果不同?
为什么两种列表翻倍写法对原始列表的修改行为完全不同
不少开发者初次遇到这个现象时,都会下意识认为my_list *= 2是my_list = my_list * 2的简写,两者应该完全等价。实际上这两种操作从底层实现逻辑上就存在本质差异,和Python的对象引用规则、可变类型的运算符重载设计直接相关。
两种运算的实际执行流程差异
执行
my_list = my_list * 2时,会按两步完成操作,核心动作是生成新对象+给形参重新绑定引用,全程不修改原列表内容:- 调用列表的
__mul__方法,基于原列表的所有元素,在一块新的内存地址上创建一个长度翻倍的全新列表对象 - 把当前作用域内的
my_list变量的引用,从原来绑定的旧列表,指向这个刚生成的新列表
函数内的形参my_list在刚传入时,和外部变量x指向完全相同的原始列表对象,但一旦执行了上述赋值操作,形参就被重新绑定到新对象,和外部x的引用完全脱钩,不会对原始列表做任何修改。这就是func1运行后,外部x依然是原始值[1,2,3]的核心原因。
- 调用列表的
执行
my_list *= 2时,因为列表是可变类型,它重载了__imul__(原地乘)魔术方法,核心动作是在原对象的内存块上直接修改内容,全程不创建新对象:直接在原列表的内存块上,把原有元素的副本追加到列表尾部,将原列表的长度直接翻倍。
整个过程没有改变任何变量的引用指向,不管是函数里的形参my_list,还是外部的x,都还指向同一个被修改过的原列表对象,所以外部打印时就能看到列表已经被翻倍。
直观的差异验证
如果想直接看到两种操作的行为区别,可以在函数中通过id()函数打印对象的内存地址,对比操作前后的地址变化:
def func1(my_list): print("进入func1时my_list的对象id:", id(my_list)) my_list = my_list * 2 print("执行赋值翻倍后my_list的对象id:", id(my_list)) def func2(my_list): print("进入func2时my_list的对象id:", id(my_list)) my_list *= 2 print("执行原地翻倍后my_list的对象id:", id(my_list)) x = [1,2,3] print("外部原始x的对象id:", id(x)) func1(x) func2(x)
运行结果可以清晰看到:
func1内部执行完my_list = my_list *2后,打印的对象id和进入函数时完全不同,说明形参已经指向了新创建的列表对象func2执行完my_list *=2后,打印的对象id和外部x的id完全一致,说明全程没有创建新对象,所有修改都是在原列表的内存块上完成的
补充说明
- 这个行为不是Python“特殊传参规则”导致的:Python所有参数传递本质都是传递对象的引用。如果是在函数内部对对象做原地修改(比如修改列表元素、使用
+=/*=这类原地操作符),所有指向该对象的引用都会看到修改;如果是给形参变量重新绑定新的对象,就不会影响外部的原始变量。 - 两种写法的行为差异只存在于可变对象上:如果是整数、元组这类不可变对象,因为无法在原内存块上做原地修改,执行
a *=2的逻辑和a = a*2完全一致,都会生成新的对象,不会出现上述的差异。
内容的提问来源于stack exchange,提问作者sshrey47
相关产品推荐
相关产品推荐

