python传参是什么意思_Python传参传什么?

最近老收到关于Python荐书的问题的邀请,以我的资历,实在不敢为大家推荐什么书籍,只能为大家分享一点点心得。

本文向大家推荐一本读过后至今仍不时翻阅的书:《编写高质量代码:改善Python程序的91个建议》kindle里的截图

这本书为Python工程师写出更好的Python代码提出了91个建议,这些建议通俗易懂、切实可行,在实践中效果立竿见影。书中的代码量较少,而且书的篇幅也比较小,所像我这样购买一份电子版的就好了。

这篇文章中与大家分享书中一节:《建议31:记住函数传参既不是传值也不是传引用》,是这本书中的第31个建议。这一节解释了Python函数中传参传的是什么的困惑,说明了Python函数中的传参,既不是传值,也不是传引用,并用简短的实例生动地说明了是不是、为什么不是。

以下是全文大部分内容转载(转载仅为分享和推荐,侵删):

建议31:记住函数传参既不是传值也不是传引用

Python中的函数参数到底是传值还是传引用呢?这是许多人在学习过程中会纠结的一个问题,很多论坛也有这样的讨论。总结来说基本有3个观点:传引用;传值;可变对象传引用,不可变对象传值。这3个观点到底哪个正确呢?我们逐一讨论。

1)传引用

先来看一个非常简单的例子(请不要因为例子太简单而不以为然,小故事往往蕴含大道理,它照样能说明问题)。

示例一:

>>> def inc(n):

... print(id(n))

... n = n + 1

... print(id(n))

...

>>>

>>> n = 3

>>> print(id(n))

140729534747520

>>> inc(n)

140729534747520

140729534747552

>>> 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

...

>>> o_list = ['a', 'b', 'c']

>>> new_list = change_list(o_list)

orginator list is: ['a', 'b', 'c']

new list is: ['a', 'b', 'c', 'i am new']

>>> print(new_list, o_list)

['a', 'b', 'c', 'i am new'] ['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] = '***'

... print(new_list)

... print(id(new_list))

...

>>> test1 = [1, ['a', 1, 3], [2, 1], 6]

>>> change_me(test1)

2564672584904

2564672584904

[1, '***', '***', 6]

2564672584904

>>> print(test1) # 函数内部对参数的作用确实反映到了原list上

[1, '***', '***', 6]

>>>

>>> test2 = [1, 2, 3, 4, 5, 6, [1]]

>>> change_me(test2)

2564672639496

2564672639496

['a', 'b', 'c']

2564672660616

>>> print(test2) # 函数内部对参数的作用没有反映到原list上

[1, 2, 3, 4, 5, 6, [1]]

>>>

传入参数org_list为列表,属于可变对象,按照可变对象传引用的理解,new_list和org_list指向同一块内存,因此两者的id值输出一致,任何对new_list所执行的内容的操作会直接反应到org_list,也就是说修改new_list会导致org_list的直接修改,对吧?来观察测试例子。

对于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++/go等语言中的赋值的意思并不一样。

Go语言中的赋值

我们观察如下程序(Go语言):

package main

import "fmt"

func main() {

var a int

var b int

a = 5

fmt.Println("a的地址是:", &a)

b = a

fmt.Println("b的地址是:", &b)

b = 7

fmt.Println("重新赋值后的b的地址是:", &b)

}

运行程序:

$ go run memory.go

a的地址是: 0xc00000a0c0

b的地址是: 0xc00000a0c8

重新赋值后的b的地址是: 0xc00000a0c8

同样的程序我们观察下Python中的输出:

>>> a = 5

>>> id(5)

140729534747584

>>> b = a

>>> id(b)

140729534747584

>>> b = 7

>>> id(b)

140729534747648

可以观察到,在Go语言中,a、b的地址一开始就不同,当为b再次赋值时,也没有改变b的地址。在Python中,一开始a、b的地址是一致的,当为b再次赋值时,b的地址发生改变。

从输出可以看出,在Python语言中,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。

Python函数传参的真相

理解了上述背景,再回头来看看前面的例子就很好理解了。

对于示例一,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)。函数参数在传递的过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,调用者和被调用者之间共享这个对象,而对于不可变对象,由于并不能真正被修改,因此,修改往往是通过生成一个新对象然后赋值来实现的。

后记:

原文中其它语言中的示例用的是C/C++,转载时换成了Go。在Go语言中, 一切传递都是值传递,要操作原对象可以通过指针操作。在Python语言中,没有指针的设计,许多Python学习者又是非科班出身且Python是其第一语言,对内存布局缺乏了解,所以在函数参数传递上有一定的困惑(甚至不知道自己应当在此处困惑)。有的学习者知道不应当把可变对象作为函数默认参数、也知道在函数内部操作可变对象有很多坑,但具体为什么可变对象要这样小心翼翼,却不得而知了。学习了《第31个建议》,就会变得豁然开朗。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值