编写高质量Python (第24条) 用 None 和 docstring 来描述默认值会变的参数

第 24 条 用 None 和 docstring 来描述默认值会变的参数

​ 有时,我们想让那种不能够提前固定的值,当作关键字参数的默认值。例如,记录日志消息时,默认的时间应该是触发事件的那一刻。所以调用者没有明确指定时间,那么默认就把调用函数当成那条日志的记录时间。现在试试下面这种写法,假定它能让 when 参数的默认值随着这个函数的执行时间而发生变化。

from time import sleep
from datetime import datetime


def log(message, when=datetime.now()):
    print(f'{when}: {message}')


log('Hi there')
sleep(0.1)
log('Hello again')

>>>
2023-11-29 13:52:14.887288: Hi there
2023-11-29 13:52:14.887288: Hello again

​ 这样写不行。因为 datetime.now 只执行了一次,所以每条日志的时间戳相同。参数的默认值只会在系统加载这个模块的时候,而不会在每次执行时都重新计算,这通常意味着这些默认值在程序启动后,就已经定下来了。只要包含这段代码的那个模块已经加载进来,那么 when 参数的默认值就是加载计算的那个 datetime.now(), 系统不会重新计算。

​ 要想在 Python 里实现这种效果,惯用的办法是把参数的默认值设为 None,同时在 docstring 文档里面写清楚,这个参数为 None时,函数怎么运作(参见 第84条)。给函数写实现代码时,要判断该参数是不是 None,如果是,那就把它改成相应的默认值。

def log(message, when=None):
    '''Log a message with a timeastamp
    
    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}')

​ 这次,两条日期的时间戳就不同了。

log('Hi there')
sleep(0.1)
log('Hello again')

>>>
2023-11-29 14:01:39.772803: Hi there
2023-11-29 14:01:39.878047: Hello again

​ 把参数的默认值写成 None 还有一个特殊的意义,就是用来表示那种以后可能由调用者修改内容的默认值(例如某个可变的容器)。例如,我们要写一个函数对采用 JSON 格式编码的数据解码。如果无法解码,那么就返回调用时所指定的默认结果,假如调用者当时没有明确指定,那就返回空白的字典。

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

​ 这样的写法与前面的 datetime.now() 的例子有着同样的问题。系统只会计算一次 default 参数(在加载这个模块的时候),所以每次调用这个函数时,给调用者返回都是一开始分配的那个字典,这就相当于是以默认值调用这个函数的代码都共用同一份字典。程序运行会出现很奇怪的效果。

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo: ', foo)
print('Bar: ', bar)

>>>
Foo:  {'stuff': 5, 'meep': 1}
Bar:  {'stuff': 5, 'meep': 1}

​ 我们本意是想让两次调用操作得到两个不同的空白字典,每个字典都可以用来存放不同的键值。但实际上,只要修改一个其中一个字典,另一个字典的内容就会受到影响。这种错误的根源在于,foo 和 bar 根本上是同一个字典,都等于系统一开始给 default 参数确定默认值时所分配的那个字典。它们表示的是同一个字典对象。

assert foo is bar

​ 要解决这个问题,可以把默认值设成 None,并且在 docstring 文档里说明,强调这个值为 None 时会怎么做。

def decode(data, default=None):
    '''Load JSON data from a string.

    Args:
        data: JSON data to decode.
        default: Defaults to an empty dictionary.
    '''
    try:
        return json.loads(data)
    except ValueError:
        if default is None:
            default = {}
        return default

​ 这样写,再运行刚才那段测试代码,就可以得出预期的结果了。

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo: ', foo)
print('Bar: ', bar)
assert bar is not foo


>>>
Foo:  {'stuff': 5}
Bar:  {'meep': 1}

​ 这个思路跟类型注解搭配起来(参见 第90条)。下面这种写法把 when 参数标注成可选(Optional)值,并限定其类型为 datetime。于是,它的取值就有两种可能,要么是 None,要么是 datetime 对象。

from typing import Optional


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 occcurred.
            Defaults to the present time.
    """
    if when is None:
        when = datetime.now()
    print(f'{when}: {message}')

你可能感兴趣的:(Effective,Python,python,开发语言)