一, 学弟的困惑
十天前一个夜阑人静, 月明星稀的夜晚, 我和我的朋友们正在学校东门的小餐馆里吃着方圆 3 里内最美味的牛蛙, 唱着最好听的歌儿, 畅聊人生的意义. 突然, 我的手机一震, 气氛瞬间就安静下来, 看着牛蛙碗里三双贪婪的筷子, 我犹豫了: 不 -- 我的肉... 但是本着不让人久等的原则, 我不舍地放下了筷子. 点亮屏幕, 我的眉头不禁紧锁, 事情好像并不简单...
什么, 还上升到了去医院的程度? 现在的年轻人怎么了, 怎么那么不注意安全, 嗨, 真是一届不如一届了, 不过也好, 没受伤就好... 正当我沉浸在我自己的瞎想时, 一张图片紧接着医院那条发了过来... 嗯? 好熟悉的图!
嗯..., 这不是 PyCharm 嘛... 原来是 Python... 啊不, 我的牛蛙... 当我还在想这会是个啥问题时, 学弟发出了追问三连:
我是谁? 我从哪里来? 我的牛蛙怎么没了?
右手无意思地点开了那张承载着学弟追问三连的图, 我倒要看看, 什么问题耽误了我吃肉的最佳时机.
忽略学弟那莫名其妙的文件命名, 以及那三位数的行数, 学弟的问题由六行代码引出:
- def li_si(a,ls=[]):
- ls.append(a)
- return ls
- print(li_si(7))
- print(li_si(15))
- print(li_si(45,[1,5,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 之函数默认参数及注意点
来源: https://www.cnblogs.com/magicxyx/p/10714072.html