python函数调用时参数传递方式
python函数调用时参数传递方式
C/C++参数传递方式
对于C程序员来说,我们都知道C在函数调用时,采用的是值传递,即形参和实参分配不同的内存地址,在调用时将实参的值传给实参,在这种情况下,在函数内修改形参并不会影响到实参,但是这样带来一个问题,如果我们需要刻意地对实参进行修改,就不得不传递实参的指针到函数,然后在函数中修改指针指向的数据,以达到修改实参的目的。
后来,C++中引入了引用这个概念,即在函数定义时,在形参前加一个&符号,表示传递参数的引用,在写法上,除了多出一个&符号,其他部分和C中传值调用一样,但是实际确是达到了可以在函数内修改实参内容的目的。这种参数传递的方式被称为传引用。
python的参数传递
说完了C/C++的参数传递方式,那么python中参数传递到底是传值还是传引用呢?我们来看两个实例:
test1.py:
def test(num):
num += 10
x = 1
test(x)
print x
输出结果:
1
test2.py:
def test(lst):
lst[0] = 4
lst[1] = 5
tlist = [1,2]
test(tlist)
print tlist
输出结果:
[4,5]
可以看到,在上述代码test1.py中,在函数中修改传入的x的值,函数执行完之后,x并没有改变,至少对于int型变量而言,python函数调用为传值。
在代码test2.py中,在函数中修改传入的tlist的值,函数执行完,list的内容却被函数修改了,从这里又可以看出,对于list类型而言,python函数调用为传引用。
看到这里就很疑惑了,python的函数调用到底是传值还是传引用?
python的变量内存模型
要搞清楚python的函数调用时传值还是传引用,这还得从python的变量内存模型说起,作为一个C/C++程序员,对于变量的理解就是CPU会为每个变量分配独立的内存空间,在变量生存周期结束时内存空间被收回。
但python却使用了另一种完全不同的机制,对于python而言,一切皆对象,python为每个对象分配内存空间,但是并非为每个变量分配内存空间,因为在python中,变量更像是一个标签,就像在现实生活中,一个人可以有多种身份标签,比如:XX的父亲,XX的儿子,XX的工程师,X地志愿者等等,但对应的实体都是同一个人,只占同一份资源。
为了验证这个问题,我们可以做下面的实验:
x = 1
print(id(x))
print(id(1))
print(id(5))
x= 5
print(id(x))
print(id(1))
print(id(5))
输出:
166656176
166656176
166656128
166656128
166656176
166656128
从输出可以看出,当我们将x变量的值由1变成5时,x的地址刚好从对象1的内存地址变成了对象5的内存地址。
Tips:
对于C/C++程序员来说,这段代码并不好理解,变量的值被改变时,竟然是变量的地址变化而不是原变量地址上的值变化!而且,为什么系统会为字面值1和5分配内存空间,这在C/C++中是不存在的!
所以我们要从python变量内存角度来理解:对象1和对象5早就在内存中存在,而变量x先是指向1的标签,在赋值后变成了指向5的标签。
更多内存细节可以参考我另一篇博客:python变量的内存机制 。
python的间接引用机制
可变类型和不可变类型
在python中将类型分为了可变类型和不可变类型,分别有:
可变类型:列表,字典
不可变类型:int、float、string、tuple
我们可以这样简单地来理解可变类型和不可变类型:在修改该类型变量时是否产生新对象,如果是在原对象上进行修改,为可变对象,如果是产生新的对象,则是不可变对象。
那么怎么判断是否产生新的对象呢?我们可以用python内建id()函数来判断,这个函数返回对象在内存中的位置,如果内存位置有变动,表明变量指向的对象已经被改变。
python传参时可变类型和不可变类型的区别
事实上,对于python的函数传递而言,我们不能简单地用传值或者传址来定义参数传递,我们从上一部分中可变类型和不可变类型的角度来分析:
- 在参数传递时,实参将标签复制给了形参,这个时候形参和实参都是指向同一个对象。
- 在函数内修改形参,
- 对于不可变类型变量而言:因为不可变类型变量特性,修改变量需要新创建一个对象,形参的标签转而指向新对象,而实参没有变
- 对于可变类型变量而言,因为可变类型变量特性,直接在原对象上修改,因为此时形参和实参都是指向同一个对象,所以,实参指向的对象自然就被修改了。
到这里,应该就不难理解为什么在"python的参数传递"部分,test1.py和test2.py执行完两种完全不同的结果了(test1.py传入不可变类型int,实参未被函数修改。而test2.py传入可变类型list,实参被修改)。
不仅仅是参数传递
在上面我们谈论了可变参数和不可变参数在参数传递时的行为,其实,这种机制存在于整个python环境中,而不仅仅是参数传递中,我们看下面的例子:
list1 = [1,2]
list2 = list1
list1[0] = 3
print list1
print list2
输出:
[3, 2]
[3, 2]
在上述示例中,令list2 = list1,因为根据之前的理论,list2和list1指向同一个对象,所以修改list1时同时也会修改list2,这种机制在一定程度上可以提高资源的重复利用,但是对C/C++程序员来说无疑算是一个陷阱。
可变类型和不可变类型混合的情况
我们人为地将变量分为可变类型和不可变类型,然后分类讨论,以为就万事大吉了,但是实际情况总是复杂的,我们可以来看看下面的例子:
lst = [(1,2),3]
tup = ([1,2],3)
像这种情况中,两种类型糅杂在一起,那怎么去区分他们到底属于哪个阵营呢?这样的变量在修改时会不会创建新的对象呢?我们再来试一试:
tup = ([1,2],3)
tup1 = tup
tup1[0][0] = 3
print tup
print tup1
输出结果:
([3,2],3)
([3,2],3)
这个结果就很有意思了,元组的第一个元素为一个列表,且令tup1 = tup,即两个变量指向同一个对象,我们修改了元组的第一个元素(列表)的第一个元素,结果两个元组的数据都变了。由此引出两个问题:
- 为什么元组内的数据可以修改?
- 作为一个不可变类型变量,为什么我们修改元组的成员时,不会创建一个新的对象而是在原对象上修改,导致另一个指向这个对象的变量取值也发生了变化?
对于这个问题,我们可以这样去理解:在定义tup变量时,此前内存中没有tup对象,所以系统需要新建一个tup对象,对于tup[0]即[1,2],系统会再去找是否存在这样的对象,如果存在,直接引用原对象,如果不存在,再创建新的对象,对于tup[1]即3,也是同样的道理。
所以,事实上,tup变量只是一个引用,tup[0]同时也只是个引用,tup[1]照样如此,所以,创建一个符合类型的变量并非我们所想的在内存中专门开辟一片区域来放置整个对象,而是在不断地引用其他对象。
所以,对于问题1,为什么元组内的数据可以修改?答案是,整个元组并非一个统一的整体,我们修改的是tup[0]的元素,即一个列表变量,自然可以修改,
注意:[0,1]这个整体属于元组的元素,是不可修改的,即我们不能将[0,1]整体替换成其他,但是单独的0,1属于列表的元素,是可以修改的。
对于问题2,既然我们修改的列表的元素,列表是可变参数类型,那么自然在原对象上修改而非创建新对象。
现实中的各种情况
上面说到了python传递参数的特性,那么如果我们要在函数中修改一个不可变对象的实参,又或者是在函数中不修改可变类型的实参,那该怎么做呢?
首先,如果要在函数中修改一个不可变参数的实参,最简单也最实用的办法就是传入这个参数同时返回这个参数,因为虽然是同一个变量,在传入和返回时这个变量已经指向了不同的对象:
def test(num): #num参数指向对象5
mum += 10 #num参数指向新对象15
return num #返回num,此时num为15
print test(5)
然后,如果要在函数中不修改可变参数的实参,这个时候就需要引用另一个模块:copy,就是重新复制出另一个可变类型参数,我们可以看下面的例子:
import copy
lst = [1,2]
lst_cp = copy.copy(lst)
print id(lst)
print id(lst_cp)
输出结果:
3072211148
3072211340
从上述示例可以看出,copy过程中系统复制了一个新的对象,而不是简单地引用原来对象(两个变量指向数据地址不一样)。
但是需要注意的是,copy只对可变类型变量才创建新的对象,而对不可变类型变量,并不创建新的对象,大家可以去试试。
copy复制依然可能存在问题
在可变类型和不可变类型混合的情况下,我们知道了,一个变量很可能并非仅仅指向一个完整的对象,变量的子元素依然存在引用的情况,比如在lst = [[1,2],3)]中,lst[0]就是引用了别处的对象,而非在内存中完全存在一个单独的[[1,2],3]对象,那么,如果使用copy对lst进行复制,对于lst[0],是仅仅复制了引用,还是复制了整个对象呢?我们可以看下面的例子:
import copy
lst = [[1,2],3]
lst_cp = copy.copy(lst)
print id(lst)
print id(lst_cp)
print id(lst[0])
print id(lst_cp[0])
输出结果:
3072042988
3072043404
3072043020
3072043020
从结果可以看出,对于lst列表,仅仅是复制了lst对象,lst[0]仅仅是复制了引用。这其实违背了我们的本意,如果我们要完整地复制整个对象,那又改怎么做呢?
deepcopy
对于上面问题的解决方案是:使用copy模块的deepcopy方法,相对于copy方法(通常被称为浅拷贝),deepcopy(通常被称为深拷贝),浅拷贝就像上述的例子一样,复制出一个新的对象,但是目标对象中的子对象可能是引用。而deepcopy则是完全复制一个对象的所有内容。
好了,关于python函数调用时参数调用方式的讨论就到此为止了,如果朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言
个人邮箱:linux_downey@sina.com
原创博客,转载请注明出处!
祝各位早日实现项目丛中过,bug不沾身.
(完)
python函数调用时参数传递方式的更多相关文章
- Python入门:参数传递方式
这是关于Python的第5篇文章,主要介绍下参数传递方式和如何设计自己的函数. (一) 本篇主要介绍2种参数传递方式. 位置参数 调用函数时,根据函数定义的参数位置来传递参数. def right_t ...
- python的函数参数传递方式
python的一切数据类型都是对象.但是python的对象分为不可变对象和可变对象.python的变量是引用,对python变量的赋值是引用去绑定该对象. 可变对象的数据发生改变,例如列表和字典,引用 ...
- python运行时参数m的作用
不加m时,当前目录是py文件的所在目录 加m时,当前目录就是当前目录
- python函数调用时传参方式
位置参数 位置参数需与形参一一对应 def test(a,b) #a,b就是位置参数 print(a) print(b) test(1,2) 关键字参数 与形参顺序无关 def test(x,y) ...
- python函数的参数问题
语法 def functionname( parameters ): "函数_文档字符串" function_suite return [expression] 参数问题 必备参数 ...
- C++中函数调用时的三种参数传递方式详解
在C++中,参数传递的方式是“实虚结合”. 按值传递(pass by value) 地址传递(pass by pointer) 引用传递(pass by reference) 按值传递的过程为:首先计 ...
- C++中函数调用时的三种参数传递方式
在C++中,参数传递的方式是“实虚结合”. 按值传递(pass by value) 地址传递(pass by pointer) 引用传递(pass by reference) 按值传递的过程为:首先计 ...
- 产品经理学Python:参数传递方式
这是关于Python的第5篇文章,主要介绍下参数传递方式和如何设计自己的函数. (一) 本篇主要介绍2种参数传递方式. 位置参数 调用函数时,根据函数定义的参数位置来传递参数. def right_t ...
- Python 关于Python函数参数传递方式的一点探索
关于Python函数参数传递方式的一点探索 by:授客 QQ:1033553122 实践代码 #!/usr/bin/env python # -*- coding:utf-8 -*- __author ...
随机推荐
- protobufjs@6.8.8 postinstall: `node scripts/postinstall`
由于Node.js 版本太低了, 使用最新版用 Node.js =================================== 以下解决方法来源于网络 npm ERR! Windows_NT ...
- SDN实验---Ryu的应用开发(四)基于跳数的最短路径转发原理
一:实现最短跳数转发 (一)原理 推文:迪杰斯特拉算法和弗洛伊德算法 二:代码实现 (一)全部代码 from ryu.base import app_manager from ryu.controll ...
- Qt编写安防视频监控系统16-设备播放
一.前言 设备播放模块是后面增加的,核心就是通过组合rtsp视频流地址来播放实时视频和历史视频,目前市面上很多厂家比如排第一的海康都是支持直接rtsp通过NVR来播放某个通道视频流和回放某个通道的视频 ...
- 配置windows live writer
下载地址 https://pan.baidu.com/s/1WVpLQEadIHN15W2DIhWh4A 安装流程参考博客 https://www.cnblogs.com/haseo/p/376232 ...
- springboot:自定义缓存注解,实现生存时间需求
需求背景:在使用springbot cache时,发现@cacheabe不能设置缓存时间,导致生成的缓存始终在redis中. 环境:springboot 2.1.5 + redis 解决办法:利用AO ...
- WINGIDE 激活失败
WINGIDE 7.1 激活失败 WINGIDE 7.0 激活成功 1 下载 https://www.7down.com/soft/94270.html 2 安装 3 激活 step 1: st ...
- spring boot的actuator
actuator官方的介绍 Spring Boot includes a number of additional features to help you monitor and manage yo ...
- ThinkPHP3(命名空间、RBAC)
命名空间 当开发大型项目的时候,可以会需要成千上万的文件 面向对象通过命名空间来解决这个问题的. PHP命名空间是PHP5.3以后才出现的. 命名空间中可以出现:类,函数,常量 只有const定义的常 ...
- 嵌入式02 STM32 实验05 蜂鸣器
蜂鸣器:是一种一体化结构的电子讯响器.主要分为分压式蜂鸣器和电磁式蜂鸣器两种类型. 一.有源/无源蜂鸣器(不是指是否带电源,而是有没有自带震荡电路) 1.有源蜂鸣器:有源蜂鸣器自带震荡电路,一通电就会 ...
- Java-手动搭建SSH(maven版)
创建maven项目 把maven项目变为动态网站,步骤如下: 项目结构图如下: 开始搭建spring+springmvc+Hibernate项目 环境版本就不多说了,直接贴出pom.xml文件 < ...