[Python]可变类型,默认参数与学弟的困惑

一、学弟的困惑

    十天前一个夜阑人静、月明星稀的夜晚,我和我的朋友们正在学校东门的小餐馆里吃着方圆3里内最美味的牛蛙,唱着最好听的歌儿,畅聊人生的意义。突然,我的手机一震,气氛瞬间就安静下来,看着牛蛙碗里三双贪婪的筷子,我犹豫了:不——我的肉…但是本着不让人久等的原则,我不舍地放下了筷子。点亮屏幕,我的眉头不禁紧锁,事情好像并不简单…

    什么,还上升到了去医院的程度?现在的年轻人怎么了,怎么那么不注意安全,嗨,真是一届不如一届了,不过也好,没受伤就好…正当我沉浸在我自己的瞎想时,一张图片紧接着医院那条发了过来…嗯?好熟悉的图!

 

    嗯…,这不是PyCharm嘛…原来是Python…啊不,我的牛蛙…当我还在想这会是个啥问题时,学弟发出了追问三连:

    我是谁?我从哪里来?我的牛蛙怎么没了?

    右手无意思地点开了那张承载着学弟追问三连的图,我倒要看看,什么问题耽误了我吃肉的最佳时机。

    忽略学弟那莫名其妙的文件命名,以及那三位数的行数,学弟的问题由六行代码引出:

  1. def li_si(a,ls=[]):  
  2.     ls.append(a)  
  3.     return ls  
  4. print(li_si(7))  
  5. print(li_si(15))  
  6. print(li_si(45,[1,5,7]))  
  7. print(li_si(78))  

    一个函数,两个参数,其中一个是默认的空列表,函数里,列表对第一个参数执行append操作,返回列表。

    四个print(),每个print()的参数是一个函数调用,第一二四个函数调用只有一个参数,第二个参数使用的默认值。

    这会有啥问题?结果是显而易见的嘛。

看来学弟进度有点慢啊。这么基础的知识,怎么会扯上这么多,什么"局部变量",什么"全局变量",还有"参数"之类,引得我嘴角上扬,感觉空气中充满了快活的空气。

我夹起了一块牛蛙肉,真香。

瞄了一眼程序的输出结果,瞳孔瞬间放大。

          不好,有诈!我仿佛听到一声惊雷,右手一抖,我的牛蛙掉到了大白菜汤里,啊,牛蛙,你还是想回家啊。

          哈哈,顾不得牛蛙了,看来学弟提了一个好问题,C语言里那一套规则似乎不起作用了。

          放下筷子,虔诚的拿起了可以打开未知世界大门的手机,思绪进入计算机世界,这几行代码在执行时,到底发生了什么。

二、C语言里的函数调用

    当编译器遇到一个函数调用时,它产生代码传递参数并调用函数。C语言里所有的参数均以"传值调用"方式传递,而对于数组参数,传递的则是常量指针(数组)的拷贝。每次函数调用时,被调用的函数都有自己独有的栈空间,里面存储了函数的参数、局部变量等信息,函数返回后,栈空间被释放。

    而Python的解释器是用C写的,Python里的list底层就是C语言的可变数组,就是一个指针。

    基于这种认知,我设想的运行结果应该是,第一二四个函数使用的默认参数list,每次调用时,默认参数都回有一个值,这个值是不确定的(后面会提到,在Python里,可变类型竟然还真是确定的),所以每次调用时默认参数都(应该)指向空的数组,结果应该就是返回只有a一个元素的列表。

    但是现在运行结果显示,这三次函数调用时似乎指向了同一个列表,这就奇怪了。

三、我的猜想

    本身应该是局部变量的参数,运行时却有了全局变量的效果(我终于还是提到了学弟问的那几个词…),看着代码,我有了这样几个猜测…

    猜想1: 学弟这几行代码所在行数为106-112,有没有可能在之前的代码中,ls已经被定义过了,所以在后面的代码中,全局的ls覆盖了局部的ls,造成了这种参数全局的效果。

    猜想2: 现在我也好奇当时我为嘛会想到这个…这解释器怎么可能会跨行优化这种…可能是被牛蛙冲昏了头脑。

    猜想3: 这个我做过实验,对同一个函数多次调用,每次函数局部变量的地址都相同。所以我怀疑,默认参数所在内存区域的值,一直没被修改,所以每次都一样。不过这样就有了一个悖论,第三次函数调用没有使用默认的参数,内存区域的值理应被修改,但是第四次调用时又回到了前两种情况。

四、放"码"过来

    回到学校后,终于有机会能实际跑跑这奇怪的代码了,毕竟脑子不能编译、解释代码,还是要上机。

    首先,直接跑这7行代码,看看结果。

    嗯,和学弟的结果一样,可以排除含有全局变量的情况1了。

    看看每次函数调用时默认参数的值与地址。

    这结果部分地验证了猜想3,每次使用默认参数时都指向了同一个地址。

换一下,默认参数改为一个数字,这不会还指同一块吧。

    嗯…还指向同一块,难不成这个默认参数的值放常量池了,怎么老是指一个地儿…啊,对象,突然想起一句话,"Python里万物皆为对象",这么想来,每一个数字都有自己单独的地址了。嗯,实验一下。

    果然,都是对象。面向对象的特性爬出了书本,以这样一种方式在我的面前刷了一波存在感。

    因此,默认的参数ls,指向的也是同一个列表对象。而想要该变量指向新的列表的话,就得重新赋值。

    重新赋值后,就得到了预期的结果。

五、可变类型与默认参数

    Python的内建标准类型有一种分类标准是分为可变类型与不可变类型:

  • 可变类型:列表、字典
  • 不可变类型:数字、字符串、元组

    变量保存的实际都是对象的引用,所以在给一个不可变类型(比如int)的变量a赋新值的时候,实际上是在内存中新建了一个对象,并讲a指向这个对象,然后将原对象的引用计数-1。

    所以当函数参数是默认列表时,它始终指向同一个对象,除非重新赋值,否则它并不会重新创建一个新列表。也就是说,多次调用函数执行append操作,实际上是对同一个对象进行操作。

 

 

参考:Python——可变类型与不可变类型(即为什么函数默认参数要用元组而非列表)

    python之函数默认参数及注意点

posted on 2019-04-15 23:19  Magic激流  阅读(1371)  评论(0编辑  收藏  举报

导航