哎,先叹口气。昨天本来想着下班前优化一个小功能,结果硬生生搞到凌晨两点,最后发现居然是一个我早就知道、但一不留神又踩进去的坑!气得我差点把桌上的咖啡泼屏幕上——幸好那是最后一杯。
事情是这样的:我在写一个函数,用来记录用户的操作日志,每次调用就往列表里加一条记录。我寻思着,为了灵活,让调用方可以自己传一个列表进来,如果没传就用默认的空列表。于是乎,我优雅地写出了下面这段代码:
def add_log(message, log_list=[]):
log_list.append(message)
return log_list是不是看起来人畜无害?我当时也是这么想的。然后我模拟了几个用户操作:
user1_logs = add_log('登录')
print(user1_logs) # 输出:['登录']
user2_logs = add_log('浏览页面')
print(user2_logs) # 期待:['浏览页面'],但实际输出:['登录', '浏览页面']哎?user2的日志怎么把user1的也带上了?我当时就懵了,难道Python偷偷开了共享内存?再试一次:
print(add_log('下单')) # 输出:['登录', '浏览页面', '下单']好家伙,这列表像吃了炫迈一样,根本停不下来!我明明没传参数,它居然把之前所有调用攒下的记录都背在身上。这要是线上环境,日志直接乱套,背锅都找不到方向。
其实这个问题,但凡有点Python经验的都知道——可变默认参数陷阱。但知道归知道,熬夜写代码脑子一热就容易忘。今天必须把这个坑给它填平,顺便把“肇事司机”拉出来游街示众。
Python函数的默认值只在函数定义的时候被计算一次,而且这个默认值会一直存在,不会每次调用都重新创建。如果默认值是可变对象(比如列表、字典、集合),那它就像个“幽灵”,在你每次调用函数时默默地跟着你。
当你修改这个默认对象时,实际上修改的是那个一直存在的“幽灵”,所以下一次调用它的时候,它已经是被改过的状态了。就像我上面的例子,第一次调用在默认列表里加了“登录”,第二次调用还是那个列表,所以“浏览页面”就被追加进去了。
既然默认列表是“幽灵”,那我们就别给它留肉身。用None作为默认值,然后在函数内部创建新列表。这样每次调用,如果没有传列表,都会得到一个崭新的列表。
看改造后的代码:
def add_log(message, log_list=None):
if log_list is None:
log_list = []
log_list.append(message)
return log_list再试试:
print(add_log('登录')) # ['登录']
print(add_log('浏览页面')) # ['浏览页面']
print(add_log('下单')) # ['下单']完美!各用各的,谁也不干扰谁。这就像每次来人,都给你新拿一个一次性纸杯,而不是用别人喝过的。
除了列表,字典、集合也是一样的道理。比如:
def add_user(user, user_dict={}):
user_dict[user] = True
return user_dict同样会累积。还有类属性、函数内部定义的函数如果引用了外层可变变量,也可能出现类似问题。总之,只要默认值是可变对象,就要多留个心眼。
其实很多编程语言里都有这种“细节魔鬼”,C++的未定义行为、JavaScript的闭包陷阱、PHP的引用……踩坑不可怕,可怕的是踩完不总结,下次继续踩。我昨晚就是典型的“脑子一抽”。所以今天我决定把这个坑写下来,既给自己立个flag,也帮兄弟们避雷。
以后写默认参数,先问问自己:这玩意儿是可变的吗?如果是,果断None走起。如果你也遇到过类似问题,欢迎评论区吐槽,让大家看看你踩过的坑,咱们一起乐呵乐呵,哦不,一起进步。
对了,如果你是刚学Python,千万别被这个吓到。Python大部分时候都很可爱,只是偶尔会皮一下。记住这个套路,以后遇到类似情况,心里就有数了。
最后,如果你觉得这篇文章有用,点个赞或者收藏一下,下次写代码前翻出来看看,保你不再被列表默认参数坑哭。毕竟,程序员何苦为难程序员?都是熬夜的兄弟!