尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

《Python 参数传递机制深度剖析:从引用到拷贝,一篇全搞定》

《Python 参数传递机制深度剖析:从引用到拷贝,一篇全搞定》 Python的参数传递机制摘要本文深入解析 Python 的参数传递机制——传递对象引用pass-by-object-reference。通过id()函数验证实参、形参和对象属性共享同一内存地址揭示“变量是对象的引用”这一核心。重点区分可变对象list、dict、set与不可变对象int、str、tuple在传参时的不同表现不可变对象因无法修改而看似“复制”可变对象则因共享引用而产生“别名副作用”。文章进一步通过“狗玩具箱”案例详细对比直接赋值、浅拷贝和深拷贝三种处理可变对象的方式并警示避免使用可变对象作为函数默认参数。掌握这些机制有助于编写更安全、可预测的 Python 代码。这篇是我之前一篇文章的后续深入学习感兴趣的朋友可以点击这里为什么 Python 必须写self.从赋值到参数传递一篇文章讲透核心概念在 Python 中所有数据类型都是对象变量只是指向对象的引用。Python没有纯粹的值传递也没有 C 那种引用传递官方对Python的参数传递机制的叫法就是传递对象引用pass‑by‑object‑reference本质就是函数调用时做一次变量赋值。当函数被调用时实参的对象引用会传递给形参函数内部操作的对象是否会影响原变量取决于对象的可变性通俗点说实参把“指向对象的地址”复制了一份交给了形参。实参和形参是两个不同的盒子但盒子里装的是同一个对象的门牌号。代码拆解使用id()函数修改的上一篇文章的示例代码classPerson:def__init__(self,name):print(f1. 形参 name 的地址:{id(name)})# 打印地址self.nnameprint(f2. 属性 self.n 的地址:{id(self.n)})# 打印地址my_name John print(f0. 实参 my_name 的地址:{id(my_name)})# 打印地址pPerson(my_name)输出结果结论外部的实参my_name、函数内部的形参name、对象的属性self.n这三个变量名指向的是同一个内存地址。赋值操作self.n name本质是把地址引用复制了一份存进了对象的属性里。字符串John本身从头到尾只有一个没有被拷贝。比喻理解把 John 看作一个盒子传参视为“贴标签”my_name John是给盒子加了my_name的标签p Person(my_name)给盒子贴上name临时标签__init__结束就撕掉self.n name给盒子贴上self.n标签关键区别可变对象和不可变对象既然大家指向同一个对象那为什么平时感觉字符串、数字像是“复制”了一份改了互不影响而列表、字典却会“互相牵连”呢原因在于对象分“可变”和“不可变”。不可变对象int, str, tuple无法修改对象本身。当执行name New时并不是修改了原来的John而是让name这个形参换了一个新的门牌号指向新字符串New。因为属性self.n还拿着旧门牌号所以看起来互不影响。classPerson:def__init__(self,name):print(f1. 形参 name 的初始地址:{id(name)})# 打印地址self.nnameprint(f2. 属性 self.n 的地址:{id(self.n)})# 打印地址nameNewprint(f3. 形参 name 的新地址:{id(name)})# 打印地址my_name John print(f0. 实参 my_name 的地址:{id(my_name)})# 打印地址pPerson(my_name)输出结果在self.n name这个场景里因为name指向的是不可变的字符串John所以无论后续在内部把name改成别的还是把外部的my_name改成别的它们只会切断自己手里的绳子去指向新对象绝对不可能修改掉内存里那个原始的John字符串。因此你的对象属性是安全的。可变对象list, dict, set可以直接修改对象内部的数据。如果形参和实参指向同一个列表形参执行append()实参那边也会跟着变。classPerson:def__init__(self,name):self.nname# 在初始化内部修改列表name.append( Mike )print(f函数内部列表变成了:{name})my_name[ John ]#使用列表举例print(f调用前外部列表:{my_name})pPerson(my_name)print(f调用后外部列表:{my_name})输出结果因为name和my_name指向同一个可变列表在__init__里改动了列表内容外部的变量自然也跟着变了这就是常见的“别名副作用”。三种拷贝方式详解直接赋值 / 浅拷贝 / 深拷贝上一节我们看到列表作为参数传入__init__使用直接赋值的方式内部append后外部也变了。那如果我就是想安全地操作传入的可变对象而不污染外部该怎么做——浅拷贝和深拷贝我们用一个真实的“狗玩具箱”案例看看三个级别直接赋值、浅拷贝、深拷贝的内存变化。注意本示例使用列表演示拷贝逻辑。不同容器支持的拷贝方式并不相同1. 直接赋值self.toys toys—— 完全共享importcopyclassDog:def__init__(self,name,toys):self.namename# 方式1直接赋值引用传递self.toystoys# 方式2浅拷贝切片或 copy.copy# self.toys toys[:]# 方式3深拷贝copy.deepcopy# self.toys copy.deepcopy(toys)# 外部定义玩具箱outside_toys[[Ball,Bell],# 玩具箱1球 铃铛[Rope,Knot]# 玩具箱2绳子 结]my_dogDog(Buddy,outside_toys)# 外部改外层outside_toys.append([Bone])# 新增一个玩具箱print(my_dog.toys)# 狗Buddy的玩具箱也会多一个。# 外部改内层outside_toys[0].append( Squeak )# 玩具箱1新增一个玩具print(my_dog.toys)# 狗Buddy的玩具箱1里也会多出一个玩具。输出结果效果self.toys和outside_toys指向完全同一个列表对象。结论毫无安全性外部随便动一下内部地动山摇。2. 浅拷贝—— 新建外层容器内层共享importcopyclassDog:def__init__(self,name,toys):self.namename# 方式1直接赋值引用传递# self.toys toys# 方式2浅拷贝切片或 copy.copyself.toystoys[:]# 方式3深拷贝copy.deepcopy# self.toys copy.deepcopy(toys)# 外部定义玩具箱outside_toys[[Ball,Bell],# 玩具箱1球 铃铛[Rope,Knot]# 玩具箱2绳子 结]my_dogDog(Buddy,outside_toys)# 外部改外层outside_toys.append([Bone])# 新增一个玩具箱print(my_dog.toys)# 狗Buddy的玩具箱不变。# 外部改内层outside_toys[0].append( Squeak )# 玩具箱1新增一个玩具print(my_dog.toys)# 狗Buddy的玩具箱1里也会多出一个玩具。输出结果我们用内存地址来拆解它做了什么外层self.toys在内存里新建了一个列表对象新地址所以它和outside_toys的外层盒子不是同一个。内层self.toys[0]和outside_toys[0]指向同一个内层列表即[Ball, Bell]的地址被复制了进去。分析一下外部修改外层outside_toys.append([Bone])→ 狗的self.toys不受影响因为外层盒子已经分开了。外部修改内层outside_toys[0].append(Squeak)→ 狗的self.toys[0]会同步增加“ Squeak ”。这就是浅拷贝的“陷阱”它只复制了第一层容器容器里的每一项依然拿着原对象的门牌号。3. 深拷贝self.toys copy.deepcopy(toys)—— 彻底独立importcopyclassDog:def__init__(self,name,toys):self.namename# 方式1直接赋值引用传递# self.toys toys# 方式2浅拷贝切片或 copy.copy# self.toys toys[:]# 方式3深拷贝copy.deepcopyself.toyscopy.deepcopy(toys)# 外部定义玩具箱outside_toys[[Ball,Bell],# 玩具箱1球 铃铛[Rope,Knot]# 玩具箱2绳子 结]my_dogDog(Buddy,outside_toys)# 外部改外层outside_toys.append([Bone])# 新增一个玩具箱print(my_dog.toys)# 狗Buddy的玩具箱不变。# 外部改内层outside_toys[0].append( Squeak )# 玩具箱1新增一个玩具print(my_dog.toys)# 狗Buddy的玩具箱1不变。输出结果效果递归地把所有层级的对象全部新建一份。外层新列表。内层新列表[Ball, Bell]在内存里重新拷贝一份。甚至更深的层只要遇到可变对象统统重新创建。测试一下无论外部怎么折腾outside_toys增删改查甚至改内层零件my_dog.toys都稳如泰山因为两个结构在内存中完全独立毫无瓜葛。类比理解把列表想象成一个带编号的储物柜外层每个柜子里放着盒子内层对象。直接赋值把你的柜子钥匙直接复制一把给朋友。朋友和你用同一个柜子你往里面塞东西朋友那儿也有。浅拷贝买了一个新柜子新地址但是把原来柜子里的旧盒子原封不动地搬进去你们俩共用。你往自己柜子里加新盒子朋友没有但你往旧盒子里装新东西朋友的盒子里同样会有。深拷贝买了一个新柜子并且把原来的旧盒子全部复制一份新的放进去。从此你们各用各的互不干扰。延伸永远不要用可变对象如[]、{}、set()作为函数的默认参数classDog:# 注意这里 [] 是在定义类时就创建的【唯一公用列表】def__init__(self,name,toys[]):self.namename self.toystoys# 直接赋值贴标签# 创建第一只狗没给玩具使用默认的空列表dog1Dog(Buddy)print(dog1.toys)# 输出[]# 给狗1的【第一个玩具】 Balldog1.toys.append(Ball)print(dog1.toys)# 输出[Ball]# 创建第二只狗依然没给玩具继续使用默认列表dog2Dog(Charlie)print(dog2.toys)# 输出[Ball] 灾难Charlie 还没玩玩具箱里居然有 Ball结果因为dog1.toys和默认参数指向同一个“公用箱子”用dog1.toys.append(Ball)修改了箱子里的内容导致后创建的dog2拿到的也是个被污染过的箱子。正确写法守门员模式把默认参数设为None在函数体内判断如果没传参就新建一个属于这个实例自己的新箱子。classDog:# 默认参数用不可变的 Nonedef__init__(self,name,toysNone):self.namename# 如果没传就创建【全新的列表】如果传了就用浅拷贝或深拷贝隔离外部iftoysisNone:self.toys[]# 每次执行到这里都是新建一个独一无二的空箱子else:self.toystoys[:]# 或者 copy.deepcopy(toys)避免外部后续修改影响实例dog1Dog(Buddy)dog1.toys.append(Ball)# 正常使用dog2Dog(Charlie)print(dog2.toys)# 输出[] 完美互不干扰
返回列表