《编写高质量Python代码的59个有效方法》——第20条:用None和文档字符串来描述具有动态默认值的参数

本节书摘来自华章社区《编写高质量Python代码的59个有效方法》一书中的第20条:用None和文档字符串来描述具有动态默认值的参数,作者[美]布雷特·斯拉特金(Brett Slatkin),更多章节内容可以访问云栖社区“华章社区”公众号查看

第20条:用None和文档字符串来描述具有动态默认值的参数
有时我们想采用一种非静态的类型,来做关键字参数的默认值。例如,在打印日志消息的时候,要把相关事件的记录时间也标注在这条消息中。默认情况下,消息里面所包含的时间,应该是调用log函数那一刻的时间。如果我们以为参数的默认值会在每次执行函数时得到评估,那可能就会写出下面这种代码。

两条消息的时间戳(timestamp)是一样的,这是因为datetime.now只执行了一次,也就是它只在函数定义的时候执行了一次。参数的默认值,会在每个模块加载进来的时候求出,而很多模块都是在程序启动时加载的。包含这段代码的模块一旦加载进来,参数的默认值就固定不变了,程序不会再次执行datetime.now。
在Python中若想正确实现动态默认值,习惯上是把默认值设为None,并在文档字符串(docstring)里面把None所对应的实际行为描述出来(参见本书第49条)。编写函数代码时,如果发现该参数的值是None,那就将其设为实际的默认值。

现在,两条消息的时间戳就不同了。

如果参数的实际默认值是可变类型(mutable),那就一定要记得用None作为形式上的默认值。例如,从编码为JSON格式的数据中载入某个值。若解码数据时失败,则默认返回空的字典。我们可能会采用下面这种办法来实现此功能:

这种写法的错误和刚才的datetime.now类似。由于default参数的默认值只会在模块加载时评估一次,所以凡是以默认形式来调用decode函数的代码,都将共享同一份字典。这会引发非常奇怪的行为。

我们本以为foo和bar会表示两份不同的字典,每个字典里都有一对键和值,但实际上,修改了其中一个之后,另外一个似乎也会受到影响。这种错误的根本原因是:foo和bar其实都等同于写在default参数默认值中的那个字典,它们都表示的是同一个字典对象。

解决办法,是把关键字参数的默认值设为None,并在函数的文档字符串中描述它的实际行为。

现在,再来运行和刚才相同的测试代码,就能产生符合预期的结果了。

要点
参数的默认值,只会在程序加载模块并读到本函数的定义时评估一次。对于{}或[]等动态的值,这可能会导致奇怪的行为。
对于以动态值作为实际默认值的关键字参数来说,应该把形式上的默认值写为None,并在函数的文档字符串里面描述该默认值所对应的实际行为。
第21条:用只能以关键字形式指定的参数来确保代码明晰
按关键字传递参数,是Python函数的一项强大特性(参见本书第19条)。由于关键字参数很灵活,所以在编写代码时,可以把函数的用法表达得更加明确。
例如,要计算两数相除的结果,同时要对计算时的特殊情况进行小心的处理。有时我们想忽略ZeroDivisionError异常并返回无穷。有时又想忽略Overf?lowError异常并返回0。

这个函数用起来很直观。下面这种调用方式,可以忽略除法过程中的f?loat溢出,并返回0。

下面这种调用方式,可以忽略拿0做除数的错误,并返回无穷。

该函数用了两个Boolean参数,来分别决定是否应该跳过除法计算过程中的异常,而问题就在于,调用者写代码的时候,很可能分不清这两个参数,从而导致难以排查的bug。提升代码可读性的一种办法,是采用关键字参数。在默认情况下,该函数会非常小心地进行计算,并且总是会把计算过程中发生的异常重新抛出。

现在,调用者可以根据自己的具体需要,用关键字参数来覆盖Boolean标志的默认值,以便跳过相关的错误。

上面这种写法还是有缺陷。由于这些关键字参数都是可选的,所以没办法确保函数的调用者一定会使用关键字来明确指定这些参数的值。即便使用新定义的safe_division_b函数,也依然可以像原来那样,以位置参数的形式调用它。

对于这种复杂的函数来说,最好是能够保证调用者必须以清晰的调用代码,来阐明调用该函数的意图。在Python 3中,可以定义一种只能以关键字形式来指定的参数,从而确保调用该函数的代码读起来会比较明确。这些参数必须以关键字的形式提供,而不能按位置提供。
下面定义的这个safe_division_c函数,带有两个只能以关键字形式来指定的参数。参数列表里的*号,标志着位置参数就此终结,之后的那些参数,都只能以关键字形式来指定。

现在,我们就不能用位置参数的形式来指定关键字参数了。

关键字参数依然可以用关键字的形式来指定,如果不指定,也依然会采用默认值。

在Python 2中实现只能以关键字来指定的参数
不幸的是,与Python 3不同,Python 2并没有明确的语法来定义这种只能以关键字形式指定的参数。不过,我们可以在参数列表中使用操作符,并且令函数在遇到无效的调用时抛出TypeErrors,这样就可以实现与Python 3相同的功能了。操作符与*操作符类似(参见本书第18条),但区别在于,它不是用来接受数量可变的位置参数,而是用来接受任意数量的关键字参数。即便某些关键字参数没有定义在函数中,它也依然能够接受。

为了使Python 2版本的safe_division函数具备只能以关键字形式指定的参数,我们可以先令该函数接受**kwargs参数,然后,用pop方法把期望的关键字参数从kwargs字典里取走,如果字典的键里面没有那个关键字,那么pop方法的第二个参数就会成为默认值。最后,为了防止调用者提供无效的参数值,我们需要确认kwargs字典里面已经没有关键字参数了。

现在,既可以用不带关键字参数的方式来调用safe_division_d函数,也可以用有效的关键字参数来调用它。

与Python 3版本的函数一样,我们也不能以位置参数的形式来指定关键字参数的值。

此外,调用者还不能传入不符合预期的关键字参数。

要点
关键字参数能够使函数调用的意图更加明确。
对于各参数之间很容易混淆的函数,可以声明只能以关键字形式指定的参数,以确保调用者必须通过关键字来指定它们。对于接受多个Boolean标志的函数,更应该这样做。
在编写函数时,Python 3有明确的语法来定义这种只能以关键字形式指定的参数。
Python 2的函数可以接受**kwargs参数,并手工抛出TypeError异常,以便模拟只能以关键字形式来指定的参数。

上一篇:计算机技能需求新排名:Python 仅排第 3,第 1 你可能猜不到哦


下一篇:PMP认证