在前一篇关于Python函数的文章中,我们介绍了函数的基本使用、函数的默认参数、lambda函数的用法,相当于对Python中的函数有了一个入门的介绍。今天这篇文章打算就上一篇提到的函数的参数默认值,进一步展开来讲。因为,这个看似简单实用的技巧,如果不理解相关的底层细节,可能反而导致意想不到的BUG。
本文先以两个应用参数默认值可能导致的问题来展开,然后探究相关问题产生的底层原理,最后给出应对的最佳实践。
日志打印的问题假如现在有这样一个日志记录的需求,我们这样简化模拟一下:1、打印日志只需要记录日志内容,及对应的时间;2、默认情况下,记录的时间,为当前日志打印的时间即可;但是,不排除业务流程处理时间较长,可能需要记录业务开始时间,而非当前时间的场景,所以要支持传入一个时间的需求。
根据上面的需求,参数默认值,是我们最先想到的,所以,可以定义如下函数:
但是,实际执行的结果,可能不是我们想要的:
执行结果:
明明等待了5秒,为啥日志打印的时间都是相同的……
返回参数默认值的问题有些情况下,我们函数需要返回一个容器对象,用户需要基于这个容器进行,进一步的操作,使用了参数默认值可能也是存在问题的。比如,有如下场景:api传入的请求参数以字符串的形式拼接在一起,我们需要将其解析为字典格式,并返回,如果这个api没有请求参数,则返回一个空字典。用户需要对返回的请求参数字典进行进一步的处理,比如从cookie中提取信息,比如userid等,加入到请求参数字典中。根据需求,可能会选择定义一个如下的函数:
正常情况下,应该都是没有问题的,但是,如果走了默认情况下,可能存在问题:
执行结果:
用户2和用户3都是无参数请求api,可是最终处理完成后,两个请求中的userid都变成了3……
问题产生的原因不管是日志打印中的默认当前时间,还是请求参数解析的返回空的参数字典,似乎都出现了我们预料之外的情况:函数的多次重复调用,默认值参数的默认值,我们以为在每次发生时,都会变化,我们理解的是无固定值的默认值,可是函数似乎给我们固定住了……
原因在于,参数默认值如果是一个表达式,这个表达式会在函数定义时,计算出来,并生成一个对象,存储下来,以后的每次调用,参数的默认值都指向一个相同的对象。
通过字节码,我们可以更加清晰地看到这一点:以日志打印为例:
通过如上的字节码与源码的对照,可以轻易发现,函数参数的默认值的计算,确实是在函数定义时完成的,函数调用时,直接取之前计算出来的结果,不会重新计算。
此外,即使不看对应的字节码,我们还有更简单的方法,来看到参数默认值的情况:由于Python中一切皆对象,函数也是一个特殊的对象,函数对象,有自身的一些属性,其中一个属性就是__defaults__,以元组的形式存储了函数的参数默认值:
如上代码,我们在调用函数log()之前,首先输出了log函数对象的__defaults__属性,然后是两次函数调用。
执行结果如下:
两次函数调用,输出的参数默认值,均为函数对象在定义时,存储在函数对象的__defaults__中的默认值。
同样的,在请求参数解析的函数中,我们定义的默认的请求参数空字典对象,也是在定义时生成的。我们可以通过查看函数对象的参数默认值对象的id,以及args2、args3的id,清楚地看到这一点:
执行结果:
可以看到,3个对象的id是相同的,印证了参数默认值在函数定义时生成对象,并存储到函数对象的__defaults__属性中的论断。
关于参数默认值的最佳实践关于以上两种场景中,涉及到参数默认值使用中的异常情况,一个相对较好的解决方案是,使用None默认值,并结合docstirng进行使用说明。同样以日志打印为例,进行代码的改写,以示说明:
执行结果:
这次执行,获得了我们想要的结果。
总结虽然函数参数的默认值,语法很简单,使用很方便。但是,稍微一不留意,可能也会导致一些异常的结果。基础很简单,但也很重要。真正掌握基础并不简单,只是把语法记住了,并不是真正掌握。遇到问题不要慌,关注底层的细节,能够更加容易的定位问题所在,并理解问题的产生。而所谓的编程学习,学的并不是写几行代码,而是通过写代码,逐渐习得并强化自己定位问题、解决问题的能力。