- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议31:记住函数传参既不是传值也不是传引用
Python中的函数参数到底是传值还是传引用呢?这是许多人在学习过程中会纠结的一个问题,很多论坛也有这样的讨论。总结来说基本有3个观点:传引用;传值;可变对象传引用,不可变对象传值。这3个观点到底哪个正确呢?我们逐一讨论。
1)传引用。先来看一个非常简单的例子(请不要因为例子太简单而不以为然,小故事往往蕴含大道理,它照样能说明问题)。
示例一:
>>> def inc(n): ... print id(n) ... n = n+1 ... print id(n) ... >>> n =3 >>> id(n) 34450040 >>> inc(n) 34450040 # 修改之前的n 的id 值 34450028 # 修改之后的n 的id 值 >>> print n 3 >>>
按照传引用的概念,上面的例子期望的输出应该是4,并且inc()函数里面执行操作n=n+1的前后n的id值应该是不变的。可是事实是不是这样的呢?非也,从输出结果来看n的值还是不变,但id(n)的值在函数体前后却不一致。显然,传引用这个说法是不恰当的。
2)传值。来看一个示例。
示例二:
>>> def change_list(orginator_list): ... print "orginator list is:",orginator_list ... new_list = orginator_list ... new_list.append("I am new") ... print "new list is:", new_list ... return new_list ... >>> >>> orginator_list = ['a','b','c'] >>> new_list = change_list(orginator_list) orginator list is: ['a', 'b', 'c'] new list is: ['a', 'b', 'c', 'I am new'] >>> print new_list ['a', 'b', 'c', 'I am new'] >>> print orginator_list ['a', 'b', 'c', 'I am new'] >>>
传值通俗来讲就是这个意思:你在内存中有一个位置,我也有一个位置,我把我的值复制给你,以后你做什么就跟我没关系了,我是我,你是你,咱俩井水不犯河水。可是上面的程序输出根本就不是这么一回事,显然change_list()函数没有遵守约定,调用该函数之后orginator_list也发生了改变,这明显侵犯了orginator_list的权利。这么看来传值这个说法也不合适。
3)可变对象传引用,不可变对象传值。这个说法最靠谱,很多人也是这么理解的,但这个说法到底是否准确呢?再来看一个示例。
示例三:
>>> def change_me(org_list): ... print id(org_list) ... new_list = org_list ... print id(new_list) ... if len(new_list)>5: ... new_list = ['a','b','c'] ... for i,e in enumerate(new_list): ... if isinstance(e,list): ... new_list[i]="***" # 将元素为list 类型的替换为*** ... print new_list ... print id(new_list) ... >>>
传入参数org_list为列表,属于可变对象,按照可变对象传引用的理解,new_list和org_list指向同一块内存,因此两者的id值输出一致,任何对new_list所执行的内容的操作会直接反应到org_list,也就是说修改new_list会导致org_list的直接修改,对吧?来看测试例子。
>>> test1 = [1,['a',1,3],[2,1],6] >>> change_me(test1) #test1 的元素个数小于5 35260216 35260216 [1, '***', '***', 6] #test1 中所有list 类型的元素都替换为了*** 35260216 >>> print test1 [1, '***', '***', 6] >>> test2=[1,2,3,4,5,6,[1]] #test1 中元素的个数大于5 >>> change_me(test2) 35260136 35260136 ['a', 'b', 'c'] 35250664 #new_list 的id 值发生了改变 >>> print test2 #test2 并没有发生改变 [1, 2, 3, 4, 5, 6, [1]] >>>
对于test1、new_list和org_list的表现和我们理解的传引用确实一致,最后test1被修改为[1, '***', '***', 6],但对于输入test2、new_list和org_list的id输出在进行列表相关的操作前是一致的,但操作之后new_list的id值却变为35250664,整个test2在调用函数change_me()后却没有发生任何改变,可是按照传引用的理解期望输出应该是['a','b','c'],似乎可变对象传引用这个说法也不恰当。
那么Python函数中参数传递的机制到底是怎么样的?要明白这个概念,首先要理解:Python中的赋值与我们所理解的C/C++等语言中的赋值的意思并不一样。如果有如下语句:
a =5 ,b = a ,b=7 ;
我们分别来看一下在C/C++以及Python中是如何赋值的。
如图3-6所示,C/C++中当执行b=a的时候,在内存中申请一块内存并将a的值复制到该内存中;当执行b=7之后是将b对应的值从5修改为7。
图3-6 C/C++赋值时内存的变化
但在Python中赋值并不是复制,b=a操作使得b与a引用同一个对象。而b=7则是将b指向对象7,如图3-7所示。
图3-7 Python中赋值语句对应的内存变化
我们通过以下示例来验证上面所述的过程:
>>> a = 5 >>> id(a) 34450016 >>> b = a >>> id(b) #b=a 之后b 的id() 值和a 一样 34450016 >>> b = 7 >>> id(b) #b=7 之后b 指向对象7 ,id() 值发生改变 34449992 >>> id(a) 34450016
从输出可以看出,b=a赋值后b的id()输出和a一样,但b=7操作后b指向另外一块空间。可以简单理解为,b=a传递的是对象的引用,其过程类似于贴“标签”,5和7是实实在在的内存空间,执行a=5相当于申请一块内存空间代表对象5并在上面贴上标签a,这样a和5便绑定在一起了。而b=a相当于对标签a创建了一个别名,因此它们实际都指向5。但b=7操作之后标签b重新贴到7所代表的对象上去了,而此时5仅有标签a。
理解了上述背景,再回头来看看前面的例子就很好理解了。对于示例一,n=n+1,由于n为数字,是不可变对象,n+1会重新申请一块内存,其值为n+1,并在函数体中创建局部变量n指向它。当调用完函数inc(n)之后,函数体中的局部变量在函数外不并不可见,此时的n代表函数体外的命名空间所对应的n,值还是3。而在示例三中,当org_list的长度大于5的时候,new_list = ['a','b','c']操作重新创建了一块内存并将new_list指向它。当传入参数为test2=[1,2,3,4,5,6,[1]]的时候,函数的执行并没有改变该列表的值。
因此,对于Python函数参数是传值还是传引用这个问题的答案是:都不是。正确的叫法应该是传对象(call by object)或者说传对象的引用(call-by-object-reference)。函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,调用者和被调用者之间共享这个对象,而对于不可变对象,由于并不能真正被修改,因此,修改往往是通过生成一个新对象然后赋值来实现的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论