Python函数的参数传递细节

一、C/C++中的函数参数传递

  在C/C++的函数参数传递大家都很好理解,在定义时的参数就是形参,调用时输入的就是实参,下面用代码来理解值传递、地址传递(引用传递):

1.1 值传递

void find(int x){}
int z = 1;
y = find(z);

  在这个例子中,x是形参,z是实参,x变z不变。在值传递过程中,实参和形参位于内存中两个不同地址中,实参先自己复制一次拷贝,再把拷贝复制给形参。所以,在值传递过程中,形参的变化不会对实参有任何的影响。

1.2 地址传递(引用传递)

void find(int &x){}
int a = 3;
int &z = a;
y= find(z)

  在这个例子中,x是形参,z是实参。但z随x而改变。在函数调用的时候,实参传递给函数的是指针地址,地址一样也就意味着实参和形参是一样的,当你的形参发生改变时,实参也会发生改变。(即形参和实参都指向同一个值,当值变化了,那么形参和实参也就都变化了)

二、Python中的函数参数传递

  在 C/C++ 中,传值和传引用是函数参数传递的两种方式,那么在Python中参数是如何传递的?为了回答这个问题,首先先看一段代码:

def fun1(arg):
    arg = 2
    print(arg)

a = 1
fun1(a)  # 输出:2
print(a) # 输出:1

  看到这个结果,貌似可以下结论了,是值传递稳了!但是,我们再看一段代码:

def fun2(args):
    args.append(1)

list1 = []
print(list1 )# 输出:[]
fun2(list1)
print(list1) # 输出:[1]

  有人看到这里就开始凌乱了,这什么情况。。。我们再看一段代码:

def fun2(args):
    args.append(1)
    
list1 = []
print(id(list1)) # 输出:4324106952
fun2(list1)
print(id(list1))  # 输出:4324106952

  看到这里,有人会问了,参数传递到底是传值还是传引用或者两者都不是?
  在Python中,要解释函数是什么传递就得先了解Python的一个特殊机制:可变对象和不可变对象。

2.1 可变对象和不可变对象

  Python在heap中分配的对象分成两类:可变对象和不可变对象。所谓可变对象是指,对象的内容可变,而不可变对象是指对象内容不可变。

  不可变(immutable):int、字符串(string)、float、(数值型number)、元组(tuple)
  可变(mutable):字典型(dictionary)、列表型(list)
  下面通过几个例子来理解:

i = 73
i += 2

在这里插入图片描述
  因为int类型是不可变对象,所以当执行+2操作时,其实是把73扔了,再在内存重新创建一个75,用i指向它。

>>>x = 1
>>>y = 1
>>>x = 1
>>> x is y
True
>>>y is z
True

  如上所示,因为整数为不可变,x,y,z在内存中均指向一个值为1的内存地址,也就是说,x,y,z均指向的是同一个地址,值得注意的是,整形来说,目前仅支持(-1,100)。
  总结一下不可变对象的优缺点。优点是,这样可以减少重复的值对内存空间的占用。缺点呢,我要修改这个变量绑定的值,如果内存中没用存在该值的内存块,那么必须重新开辟一块内存,把新地址与变量名绑定。而不是修改变量原来指向的内存块的值,这会给执行效率带来一定的降低。
  下面看一个可变对象的例子:

m=[5,9]
m+=[6]

在这里插入图片描述
  如上所示,可变对象的特点就是,当对可变对象进行修改时,不用重新开辟内存等操作,而是直接进行内容修改。

2.2 Python的变量与对象

  有了上面的基础,可以理解,在Python中的一切皆为对象,数字是对象,列表是对象,函数也是对象,任何东西都是对象。而变量是对象的一个引用(又称为名字或者标签,对象的操作都是通过引用来完成的。

a = []	# []是一个空列表对象,变量a是该对象的一个引用。
a.append(1)

  在 Python 中,“变量”更准确叫法是“名字”、“标签”,赋值操作“=”就是把一个名字绑定到一个对象上。就像给对象添加一个标签。

a = 1	# 整数 1 赋值给变量 a 就相当于是在整数1上绑定了一个 a 标签。
a = 2	# 整数 2 赋值给变量 a,相当于把原来整数 1 身上的 a 标签撕掉,贴到整数 2 身上。
b = a	# 把变量a赋值给另外一个变量b,相当于在对象2上贴了a,b 两个标签,通过这两个变量都可以对对象2进行操作。

  变量本身没有类型信息,类型信息存储在对象中,这和C/C++中的变量有非常大的出入(C中的变量是一段内存区域)

2.3 Python的函数参数传递

  最后就可以彻底理解Pyhton的参数传递原理了,Python 函数中,参数的传递本质上是一种赋值操作,而赋值操作是一种标签到对象的绑定过程,清楚了赋值和参数传递的本质之后,我们再来看几段代码:

def fun(arg):
    arg = 2
    print(arg)

a = 1 
fun(a)  # 输出:2
print(a) # 输出:1

  整个过程描述:一个数字对象1,一个标签a指向了数字对象1。进入fun函数时,a标签赋值给了arg标签,也就是说此时arg标签和a标签同时指向数字对象1。然后arg标签又指向了数字对象2,此时a标签和arg标签的指向不一样了,print(arg)输出的是arg标签指向的数字对象,即输出2。最后print(a)输出的是a标签指向的数字对象1,即输出1。
在这里插入图片描述
  我们来看第二段代码:

def fun(args):
    args.append(1) 
 
b= []
print(b)# 输出:[]
print(id(b)) # 输出:4324106952
fun(b)
print(b) # 输出:[1]
print(id(b))  # 输出:4324106952

  整个过程描述:一个list对象[],一个标签b指向该对象。进入fun函数时,b标签赋值给了args标签,也就是说此时args标签和b标签同时指向list对象[]。然后args标签执行了args.append(1),即list对象[]添加了一个元素1,此时执行print(b),输出[1],原因在于b和args指向同一个list对象,list对象改变,那么通过相同指向的不同标签访问对象得到同一个输出。
在这里插入图片描述

2.3 Python参数传递总结

  最后,回到问题本身,即python函数究竟是是传值还是传引用呢?说传值或者传引用都不准确。非要安一个确切的叫法的话,叫传对象(call by object)。下面来一段坑人代码来进一步说明总结:

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

  为什么说坑人呢,因为同样的调用会带来不一样的结果:

>>> print(bad_append('one'))
['one']
 
>>> print(bad_append('one'))
['one', 'one']

  分析:当第一次调用时,由于python机制原因,创建了一个list对象[],并用标签a_list指向,执行a_list.append(new_item)使得a_list指向的list对象[]添加了一个字符串’one’,然后返回a_list标签,执行输出print(bad_append(‘one’))就是print(a_list),输出a_list指向的对象[‘one’]。
  第二次调用时,由于list对象是可变对象,所以在执行a_list=[]时并没有创建新的list对象[],而是和第一次执行函数时用的同一个list对象,所以此时的a_list指向的是[‘one’],所以在执行a_list.append(new_item)后会返回[‘one’, ‘one’]。
在这里插入图片描述
  那么,我们要怎么修改这个函数来达到预定目的呢,很简单,就是把可变对象改成不可变对象即可。

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

  None是 NoneType 类的对象,并且无法修改它的值。所以每次调用函数时a_list都是指向新的None对象。
在这里插入图片描述

三、总结

  学习一门新语言时,不要总是根据已有的语言经验来代入,要深入了解新语言的特殊机制,才能避免出现太多的bug。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值