文章目录
Effective Python——编写高质量Python代码的90个有效方法
本书介绍:https://effectivepython.com/
代码demo:https://effectivepython.com/
第3章:函数
第24条 用None和docstring来描述默认值会变的参数
函数默认值的坑(函数的默认参数值只在定义函数时计算)
有时,我们想把那种不能够提前固定的值,当作关键字参数的默认值。
例如,记录日志消息时,默认的时间应该是触发事件的那一刻。
所以,如果调用者没有明确指定时间那么就默认把调用函数的那一刻当成这条日志的记录时间。
现在试试下面这种写法,假定它能让 when 参数的默认值随着这个函数每次的执行时间而发生变化。
这样写不行。
因为 datetime.now只执行了一次,所以每条日志的时间戳(timestamp)相同。
参数的默认值只会在系统加载这个模块的时候,计算一遍,而不会在每次执行时都重新计算,这通常意味着这些默认值在程序启动后,就已经定下来了。
函数的默认参数值在定义函数时只计算一次
只要包含这段代码的那个模块已经加载进来,那么when 参数的默认值就是加载时计算的那个datetime.now(),系统不会重新计算。
解决方法:将函数默认参数值设为None,然后再在函数体中判断并初始化
要想在Python里实现这种效果,惯用的办法是把参数的默认值设为None,同时在docstring 文档里面写清楚,这个参数为None 时,函数会怎么运作(参见第84条)。
给函数写实现代码时,要判断该参数是不是None,如果是,就把它改成相应的默认值。
from time import sleep
from datetime import datetime
def log(message, when=None):
if when is None:
when = datetime.now()
print(f'{when}: {message}')
log('Hi there!')
sleep(0.1)
log('Hello again!')
这次,两条日志的时间戳就不同了。
函数默认参数值设置为None的其他应用示例
把参数的默认值写成None还有个重要的意义,就是用来表示那种以后可能由调用者修改内容的默认值(例如某个可变的容器)。
例如,我们要写一个函数对采用JSON格式编码的数据做解码。
如果无法解码,那么就返回调用时所指定的默认结果,假如调用者当时没有明确指定,那就返回空白的字典。
这样的写法与前面 datetime.now 的例子有着同样的问题。
系统只会计算一次 default参数(在加载这个模块的时候),所以每次调用这个函数时,给调用者返回的都是一开始分配的那个字典,这就相当于凡是以默认值调用这个函数的代码都共用同一份字典。
这会使程序出现很奇怪的效果。
我们本意是想让这两次调用操作得到两个不同的空白字典,每个字典都可以分别用来存放不同的键值。
但实际上,只要修改其中一个字典,另一个字典的内容就会受到影响。这种错误的根源在于,foo和bar 实际上是同一个字典,都等于系统一开始给default 参数确定默认值时所分配的那个字典。
它们表示的是同一个字典对象。
import json
def decode(data, default={}):
try:
return json.loads(data)
except ValueError:
print(f'Caught a exception: [{ValueError}]')
return default
foo = decode('bad data')
foo['stuff'] = 5
print('Foo:', foo)
bar = decode('also bad')
bar['meep'] = 1
print('Bar:', bar)
assert foo is bar
要解决这个问题,可以把默认值设成None,并且在docstring文档里面说明,函数在这个值为 None 时会怎么做。
这样写,再运行刚才那段测试代码,就可以得出预期的结果了
import json
def decode(data, default=None):
"""Load JSON data from a string.
Args:
data: JSON data to decode.
default: Value to return if decoding fails.
Defaults to an empty dictionary.
"""
try:
return json.loads(data)
except ValueError:
print(f'Caught a exception: [{ValueError}]')
if default is None:
default = {}
return default
foo = decode('bad data')
foo['stuff'] = 5
print('Foo:', foo)
bar = decode('also bad')
bar['meep'] = 1
print('Bar:', bar)
assert foo is not bar
“函数默认参数值设置为None”与“函数参数类型注解”的结合应用
这个思路可以跟类型注解搭配起来(参见第90条)。
下面这种写法把when参数标注成可选(0ptional)值,并限定其类型为datetime。
于是,它的取值就只有两种可能,要么是None,要么是 datetime 对象。
from typing import Optional
from datetime import datetime
def log_typed(message: str, when: Optional[datetime] = None) -> None:
"""Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
if when is None:
when = datetime.now()
print(f'{when}: {message}')
注意:when: Optional[datetime] = None
也可以写成when: datetime | None = None
,这种写法更加清晰,但是要python3.10之后才支持。参考文章:python函数注释 typing模块函数类型注解(函数注解)(指定函数参数和返回值类型)Union注解、Callable注解、Optional可变参数注解。类型提示、类型注解、参数注释(参数类型)