Python3函数参数传递机制深度解析:可变与不可变对象的内存行为

何川
何川
发布时间:2026-10-14 | 5人浏览过
原创

本文深入解析Python3中函数参数传递机制,通过id()内存地址实验澄清常见误区。详细区分不可变对象(如整数、字符串)与可变对象(如列表、字典)在函数调用中的行为差异,解释为何修改列表内容不影响原变量指向,而重新赋值则改变引用。结合具体代码案例与钓鱼比喻,阐明Python“变量无类型,对象有类型”的核心特性,帮助开发者准确理解参数传递本质。

Python3函数参数传递机制概述

本文通过具体实例详细解析Python3中函数参数的传递机制,旨在帮助读者深入理解其工作原理,供学习参考。

在观看北理工嵩天老师主讲的Python3课程时,第五周涉及了函数调用传递的相关内容。老师提供了一个典型案例进行讲解。


#处理多个银行账户的余额信息
def addInterest(balances, rate):
  for i in range(len(balances)):
    balances[i] = balances[i] * (1+rate)
def test():
  amounts = [1000, 105, 3500, 739]
  rate = 0.05
  addInterest(amounts, rate)
  print(amounts)
test()

在该案例中,为了处理多个银行账户的数据,定义了一个名为amounts的列表。老师对此有如下说明:

“在test函数内部,首先初始化了一个包含4个数值的列表amounts,随后将该列表作为第一个参数传递给addInterest函数并执行调用,最后打印输出amounts的结果。运行结果如下:

[1050.0, 110.25, 3675.0, 775.95]

” 随后,老师得出了以下结论,引用原话如下:

“从该示例可以看出,虽然amounts变量的值看似被修改,但函数实际上无法改变变量本身,即amounts的引用关系保持不变。”

接下来对函数的执行流程进行分析,具体过程如下图所示:

对图示流程的文字解析如下:

“接下来分析整个执行过程,查看amounts输出结果的变化来源。首先,函数test的前两句代码建立了amounts和rate两个变量。随后,amounts作为实参传递给addInterest函数的形参balances。由于amounts是一个包含4个整数值的列表对象,它被传递后,函数addInterest开始执行,在0到length-1的范围内进行循环,并更新balances的值。”

关键点在于以下解释,引用原话:

“图中显示旧值[1000, 105, 3500, 739]并未直接改变,Python实际上创建了一组新值[1050.0, 110.25, 3675.0, 775.95],并使列表对象指向这组新值。旧值将在Python垃圾回收时被清除③。从图中可以清晰看出,为何包含列表参数的程序addInterest修改了列表的值?但在addInterest函数结束时,存储在amounts中的是新balances的值。实际上,变量amounts从未被重新赋值或改变其引用指向。” “它(amounts,作者注)仍然指向调用addInterest函数之前的同一个列表对象,只是当控制流返回到调用函数时,该列表对象内部呈现了被修改的状态”②

最后,老师给出了总结性结论,原话如下:

“通过上述过程我们可以了解到:Python的参数是通过值来传递的。但是如果变量是可变对象,比如是列表或者是图形对象,返回到调用程序后,该对象会呈现出被修改的状态。”

^_^ 注:课程原始视频部分结束。

可变对象与不可变对象的传递差异

阅读老师的这段讲解后,产生了一些疑问:在前面(①处)提到amounts是不能被修改的,但在(②处)又说列表呈现了被修改的状态,这是否自相矛盾?在(③)处提到列表创建了新值并使列表指向新值,这难道不是说明amounts发生了改变吗?为何又说没变?最后的结论也是列表呈现出被修改的状态。这一结论令人困惑,似懂非懂。

那么,在Python3中,当参数变量是列表时,调用函数返回后,它到底是被修改了还是没被修改?

为了澄清这个问题,我进行了一项实验。利用id()函数可以查看变量在内存中的地址,该值相当于对象的“身份证”。


# 处理多个银行账户的余额信息
def addInterest(balances, rates):
print()
print("第二处", id(balances))
  for i in range(len(balances)):
    balances[i]= balances[i]*(1+rates)
    print()
    print("第三处",id(balances))
def test():
  amounts = [1000,105,3500,739]
  print()
  print("第一处",id(amounts))
  rate = 0.05
  addInterest(amounts, rate)
  print()
  print(amounts)
  print()
  print("第四处",id(amounts))
test()

实验输出结果如下:

第一处 41203656

第二处 41203656

第三处 41203656

第三处 41203656

第三处 41203656

第三处 41203656

[1050.0, 110.25, 3675.0, 775.95]

第四处 41203656

通过该实验可以清楚看到,amounts这个对象的“身份证号码”在整个程序运行过程中从未改变,而非视频中老师所述的创建了新的列表对象。因此,amounts作为一个列表对象在程序运行过程中是被直接修改了,确实是直接被修改,而非指向新的balances值。得出这一结论的依据是查看第一处和第三处的id值:在未进入函数之前,id为41203656(第一处);进入函数之后,对象id保持不变;函数运行完毕返回后,对象id依然保持不变!

因此,结论应表述为:

改变参数值的函数行为: 实参传给形参时,Python的参数是通过值来传递的; 如果变量是可变对象(如列表或者图形对象),该对象会在函数内部被直接修改,返回到调用程序后,该对象呈现为被修改后的状态。

那么,Python3中所有的函数都采用这种传递方式吗?我们对课程视频中的另一个例子进行简单修改以验证。


 # 计算单个银行账户余额
def addinterest(balance, rate):
  print("第二处", id(balance))
  newBalance = balance * (1 + rate)
  print()
  print("第三处", id(balance))
  print()
  print("第四处", id(newBalance))
  return newBalance
def main():
  amount = 1000
  print("第一处", id(amount))
  print()
  rate = 0.05
  amount = addinterest(amount, rate)
  print()
  print("第五处", id(amount))
  print()
  print(amount)
  print("第六处", id(amount))
main()

运行结果如下:

第一处 33533648

第二处 33533648

第三处 33533648

第四处 33563344

第五处 33563344

1050.0 第六处 33563344

不是说好的直接修改吗?为何“身份证”又变了?其实这里的对象amount是一个常数,即不可变对象。当在函数中对该对象进行处理时,由于对象不可变,只能新建一个新对象,然后通过return返回新对象。

常见博客结论与内存地址实验分析

这也是目前网络上大部分博客得出的结论:

1、不可变对象作为函数参数,Python通过值传递; 2、 可变对象作为函数参数,Python通过引用传递。

注:Python中,数值类型(int和float)、字符串str、元组tuple都是不可变类型。而列表list、字典dict、集合set是可变类型。 (也有部分博客将这两个结论搞反了)

但是,也有博客提出了一个类似的例子:


def change(val):
  val = val + [10]
nums = [0, 1]
change(nums)
print(nums)

输出结果为:

[0, 1]

其实这里的代码写得不严谨,不能直接用加号添加列表元素。 可以改为如下形式:


def change(val):
  newval = [10]
  val= val + newval
nums = [0, 1]
change(nums)
print(nums)

但是输出结果依然是:

[0, 1]

难道上述结论不对吗?

参数指向其他对象时的特殊情况

其实这里需要补充另一种情况:对于可变对象作为函数参数,且参数不指向其他对象时,相当于引用传递;否则,若参数指向其他对象,则对参数变量的操作并不影响原变量的对象值。

函数里的参数变量val指向了与nums不同的内存变量,因此函数里的参数变量val不影响原变量nums的值。

**这也是因为Python的特性“变量无类型,对象有类型”。 变量是对内存空间的引用,当参数变量和原变量指向不同的内存空间时,操作互不影响。**

使用下面的代码进行验证:


def change(val):
  newval = [10]
  print("第二处",id(val))
  val = val + newval
  print("第三处",id(val))
nums = [0, 1]
print("第一处",id(nums))
change(nums)
print("第四处",id(nums))
print(nums)

运行结果如下:

第一处 39695944 第二处 39695944 第三处 39710024 第四处 39695944 [0, 1]

可以看到,第一处的nums和第二处的val内存地址完全一致。然后执行到第三处时,由于函数内val重新指向了别的内存变量,导致内存地址不同。但是最后结果要输出变量nums,即第一处和第二处内存地址的值,因此与第三处的val无关。实际上,这里的val是没有返回值的。

想要直接在列表中添加元素,可以写成如下形式:


def change(val):
  val.append(10)
nums = [0, 1]
change(nums)
print(nums)

输出结果是:

[0, 1, 10]

变量与对象关系的比喻解释

关于“变量无类型,对象有类型”可以这样理解:只有放在内存空间中的对象(也就是数据)才有类型,而变量本身是没有类型的。

如果还是不明白,可以做这样一种比喻:变量就好比钓鱼者,湖水就好像内存,里面有各种各样的鱼,它们就是对象。钓鱼者(变量)的任务就是用某种方式把自己和鱼(对象)通过鱼线连接起来。那么,鱼(对象)是有类型的,有鲢鱼、鲫鱼、带鱼。钓鱼者(变量)没有类型,他钓到不同类型的鱼(对象)。

用钓鱼的比喻解释上面的例子:


def change(val):
  newval = [10]
  val= val + newval
nums = [0, 1]
change(nums)
print(nums)

1、钓鱼人已经钓了一桶鱼,用nums桶装着,nums桶可以装很多鱼。

2、现在提着这个nums桶继续在湖里钓鱼,这时候nums桶暂时叫做装鱼桶val。突然钓鱼人钓了一条大鱼,发现装鱼桶val装不下,于是钓鱼人又在渔具店买了另一个大的装鱼桶VAL,把大鱼和之前的鱼一块装了。 钓鱼活动结束。

3、最后要看看那个叫nums的桶有哪些鱼,这时候当然只能看之前的情况。

即这个结论:对于可变对象作为函数参数,且参数不指向其他对象时,相当于引用传递;否则,若参数指向其他对象,则对参数变量的操作并不影响原变量的对象值。

同样的,针对其他两个结论,也可以用这个比喻解释:


def change( val):
  newval = val + 10
  return newval
num = 1
num = change(num)
print(num)

1、钓鱼人手上的东西num是个小蚯蚓。

2、钓鱼人拿着num去湖边钓鱼,小蚯蚓被大鱼吃了,钓鱼人钓到了一条大鱼,钓鱼人拿着鱼回家。 钓鱼活动结束。

3、问钓鱼人手上现在拿着什么东西num?当然是一条大鱼。


def change(val):
  val.append(10)
nums = [0, 1]
change(nums)
print(nums)

1、钓鱼人提着一个叫nums的桶,桶里装着2条鱼。

2、钓鱼人来到湖边钓鱼,此时桶暂时叫装鱼桶val,钓鱼人钓到了一条鱼放进装鱼桶val。 钓鱼活动结束。

3、看看钓鱼人桶里的有几条鱼。

Python参数传递总结

总结来说:

**对于不可变对象作为函数参数,相当于C系语言的值传递; 对于可变对象作为函数参数,且参数不指向其他对象时,相当于C系语言的引用传递。 对于可变对象作为函数参数,参数指向其他对象,对参数变量的操作不影响原变量的值。**

关于Python相关内容感兴趣的读者可查看本站专题:《Python函数使用技巧总结》、《Python面向对象程序设计入门与进阶教程》、《Python数据结构与算法教程》、《Python字符串操作技巧汇总》、《Python编码操作技巧总结》及《Python入门与进阶经典教程》

希望本文所述对大家Python程序设计有所帮助。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表